Introduce ability to edit a dashboard cell
* Correct documentation for dashboards
* Exclude .git and use 'make run-dev' in 'make continuous'
* Fix dashboard deletion bug where id serialization was wrong
* Commence creation of overlay technology, add autoRefresh props to DashboardPage
* Enhance overlay magnitude of overlay technology
* Add confirm buttons to overlay technology
* Refactor ResizeContainer to accommodate arbitrary containers
* Refactor ResizeContainer to require explicit ResizeTop and ResizeBottom for clarity
* Add markup and styles for OverlayControls
* CellEditorOverlay needs a larger minimum bottom height to accommodate more things
* Revert Visualization to not use ResizeTop or flex-box
* Remove TODO and move to issue
* Refactor CellEditorOverlay to allow selection of graph type
* Style Overlay controls, move confirm buttons to own stylesheet
* Fix toggle buttons in overlay so active is actually active
* Block user-select on a few UI items
* Update cell query shape to support Visualization and LayoutRenderer
* Code cleanup
* Repair fixture schema; update props for affected components
* Wired up selectedGraphType and activeQueryID in CellEditorOverlay
* Wire up chooseMeasurements in QueryBuilder
Pass queryActions into QueryBuilder so that DataExplorer can provide
actionCreators and CellEditorOverlay can provide functions that
modify its component state
* semicolon cleanup
* Bind all queryModifier actions to component state with a stateReducer
* Overlay Technologies™ can add and delete a query from a cell
* Semicolon cleanup
* Add conversion of InfluxQL to QueryConfig for dashboards
* Update go deps to add influxdb at af72d9b0e4
* Updated docs for dashboard query config
* Update CHANGELOG to mention InfluxQL to QueryConfig
* Make reducer’s name more specific for clarity
* Remove 'table' as graphType
* Make graph renaming prettier
* Remove duplicate DashboardQuery in swagger.json
* Fix swagger to include name and links for Cell
* Refactor CellEditorOverlay to enable graph type selection
* Add link.self to all Dashboard cells; add bolt migrations
* Make dash graph names only hover on contents
* Consolidate timeRange format patterns, clean up
* Add cell endpoints to dashboards
* Include Line + Stat in Visualization Type list
* Add cell link to dashboards
* Enable step plot and stacked graph in Visualization
* Overlay Technologies are summonable and dismissable
* OverlayTechnologies saves changes to a cell
* Convert NameableGraph to createClass for state
This was converted from a pure function to encapsulate the state of the
buttons. An attempt was made previously to store this state in Redux,
but it proved too convoluted with the current state of the reducers for
cells and dashboards. Another effort must take place to separate a cell
reducer to manage the state of an individual cell in Redux in order for
this state to be sanely kept in Redux as well.
For the time being, this state is being kept in the component for the
sake of expeditiousness, since this is needed for Dashboards to be
released. A refactor of this will occur later.
* Cells should contain a links key in server response
* Clean up console logs
* Use live data instead of a cellQuery fixture
* Update docs for dashboard creation
* DB and RP are already present in the Command field
* Fix LayoutRenderer’s understanding of query schema
* Return a new object, rather that mutate in place
* Visualization doesn’t use activeQueryID
* Selected is an object, not a string
* QueryBuilder refactored to use query index instead of query id
* CellEditorOverlay refactored to use query index instead of query id
* ConfirmButtons doesn’t need to act on an item
* Rename functions to follow convention
* Queries are no longer guaranteed to have ids
* Omit WHERE and GROUP BY clauses when saving query
* Select new query on add in OverlayTechnologies
* Add click outside to dash graph menu, style menu also
* Change context menu from ... to a caret
More consistent with the rest of the UI, better affordance
* Hide graph context menu in presentation mode
Don’t want people editing a dashboard from presentation mode
* Move graph refreshing spinner so it does not overlap with context menu
* Wire up Cell Menu to Overlay Technologies
* Correct empty dashboard type
* Refactor dashboard spec fixtures
* Test syncDashboardCell reducer
* Remove Delete button from graph dropdown menu (for now)
* Update changelog
pull/10616/head
parent
93a7b38043
commit
b90ff76670
cmd/chronograf
influx
ui
spec/dashboards/reducers
src
admin/components
dashboards
actions
apis
constants
containers
reducers
data_explorer
actions/view
containers
reducers
utils/influxql
hosts/containers
kapacitor/components
kubernetes/components
style
components
pages
data-explorer
|
@ -4,6 +4,8 @@
|
|||
|
||||
### Features
|
||||
1. [#1020](https://github.com/influxdata/chronograf/pull/1020): Users can now edit cell names on dashboards
|
||||
2. [#1035](https://github.com/influxdata/chronograf/pull/1035): Convert many InfluxQL statements to query builder
|
||||
3. [#1015](https://github.com/influxdata/chronograf/pull/1015): Introduce ability to edit a dashboard cell
|
||||
|
||||
### UI Improvements
|
||||
|
||||
|
|
1
Godeps
1
Godeps
|
@ -7,6 +7,7 @@ github.com/elazarl/go-bindata-assetfs 9a6736ed45b44bf3835afeebb3034b57ed329f3e
|
|||
github.com/gogo/protobuf 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b
|
||||
github.com/google/go-github 1bc362c7737e51014af7299e016444b654095ad9
|
||||
github.com/google/go-querystring 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
github.com/influxdata/influxdb af72d9b0e4ebe95be30e89b160f43eabaf0529ed
|
||||
github.com/influxdata/kapacitor 5408057e5a3493d3b5bd38d5d535ea45b587f8ff
|
||||
github.com/influxdata/usage-client 6d3895376368aa52a3a81d2a16e90f0f52371967
|
||||
github.com/jessevdk/go-flags 4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
* github.com/gogo/protobuf [BSD](https://github.com/gogo/protobuf/blob/master/LICENSE)
|
||||
* github.com/google/go-github [BSD](https://github.com/google/go-github/blob/master/LICENSE)
|
||||
* github.com/google/go-querystring [BSD](https://github.com/google/go-querystring/blob/master/LICENSE)
|
||||
* github.com/influxdata/influxdb [MIT](https://github.com/influxdata/influxdb/blob/master/LICENSE)
|
||||
* github.com/influxdata/kapacitor [MIT](https://github.com/influxdata/kapacitor/blob/master/LICENSE)
|
||||
* github.com/influxdata/usage-client [MIT](https://github.com/influxdata/usage-client/blob/master/LICENSE.TXT)
|
||||
* github.com/jessevdk/go-flags [BSD](https://github.com/jessevdk/go-flags/blob/master/LICENSE)
|
||||
|
|
2
Makefile
2
Makefile
|
@ -104,7 +104,7 @@ clean:
|
|||
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata
|
||||
|
||||
continuous:
|
||||
while true; do if fswatch -r --one-event .; then echo "#-> Starting build: `date`"; make dev; pkill chronograf; ./chronograf -d --log-level=debug & echo "#-> Build complete."; fi; sleep 0.5; done
|
||||
while true; do if fswatch -e "\.git" -r --one-event .; then echo "#-> Starting build: `date`"; make dev; pkill -9 chronograf; make run-dev & echo "#-> Build complete."; fi; sleep 0.5; done
|
||||
|
||||
ctags:
|
||||
ctags -R --languages="Go" --exclude=.git --exclude=ui .
|
||||
|
|
|
@ -75,9 +75,7 @@ curl -X POST -H "Content-Type: application/json" -d '{
|
|||
"queries": [
|
||||
{
|
||||
"label": "%",
|
||||
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
|
||||
"wheres": [],
|
||||
"groupbys": []
|
||||
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\""
|
||||
}
|
||||
],
|
||||
"type": "line"
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
func setupTestClient() (*TestClient, error) {
|
||||
if c, err := NewTestClient(); err != nil {
|
||||
return nil, err
|
||||
} else if err := c.Open(); err != nil {
|
||||
} else if err := c.Open(context.TODO()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return c, nil
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -34,12 +35,15 @@ func NewClient() *Client {
|
|||
client: c,
|
||||
IDs: &uuid.V4{},
|
||||
}
|
||||
c.DashboardsStore = &DashboardsStore{client: c}
|
||||
c.DashboardsStore = &DashboardsStore{
|
||||
client: c,
|
||||
IDs: &uuid.V4{},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Open and initialize boltDB. Initial buckets are created if they do not exist.
|
||||
func (c *Client) Open() error {
|
||||
func (c *Client) Open(ctx context.Context) error {
|
||||
// Open database file.
|
||||
db, err := bolt.Open(c.Path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
|
@ -77,7 +81,8 @@ func (c *Client) Open() error {
|
|||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// Runtime migrations
|
||||
return c.DashboardsStore.Migrate(ctx)
|
||||
}
|
||||
|
||||
// Close the connection to the bolt database
|
||||
|
|
|
@ -18,7 +18,43 @@ var DashboardBucket = []byte("Dashoard")
|
|||
// DashboardsStore is the bolt implementation of storing dashboards
|
||||
type DashboardsStore struct {
|
||||
client *Client
|
||||
IDs chronograf.DashboardID
|
||||
IDs chronograf.ID
|
||||
}
|
||||
|
||||
// AddIDs is a migration function that adds ID information to existing dashboards
|
||||
func (d *DashboardsStore) AddIDs(ctx context.Context, boards []chronograf.Dashboard) error {
|
||||
for _, board := range boards {
|
||||
update := false
|
||||
for i, cell := range board.Cells {
|
||||
// If there are is no id set, we generate one and update the dashboard
|
||||
if cell.ID == "" {
|
||||
id, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cell.ID = id
|
||||
board.Cells[i] = cell
|
||||
update = true
|
||||
}
|
||||
}
|
||||
if !update {
|
||||
continue
|
||||
}
|
||||
if err := d.Update(ctx, board); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate updates the dashboards at runtime
|
||||
func (d *DashboardsStore) Migrate(ctx context.Context) error {
|
||||
// 1. Add UUIDs to cells without one
|
||||
boards, err := d.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.AddIDs(ctx, boards)
|
||||
}
|
||||
|
||||
// All returns all known dashboards
|
||||
|
@ -51,6 +87,14 @@ func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (ch
|
|||
|
||||
src.ID = chronograf.DashboardID(id)
|
||||
strID := strconv.Itoa(int(id))
|
||||
for i, cell := range src.Cells {
|
||||
cid, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cell.ID = cid
|
||||
src.Cells[i] = cell
|
||||
}
|
||||
if v, err := internal.MarshalDashboard(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put([]byte(strID), v); err != nil {
|
||||
|
@ -85,7 +129,8 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c
|
|||
// Delete the dashboard from DashboardsStore
|
||||
func (d *DashboardsStore) Delete(ctx context.Context, dash chronograf.Dashboard) error {
|
||||
if err := d.client.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := tx.Bucket(DashboardBucket).Delete(itob(int(dash.ID))); err != nil {
|
||||
strID := strconv.Itoa(int(dash.ID))
|
||||
if err := tx.Bucket(DashboardBucket).Delete([]byte(strID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -106,6 +151,17 @@ func (d *DashboardsStore) Update(ctx context.Context, dash chronograf.Dashboard)
|
|||
return chronograf.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
for i, cell := range dash.Cells {
|
||||
if cell.ID != "" {
|
||||
continue
|
||||
}
|
||||
cid, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cell.ID = cid
|
||||
dash.Cells[i] = cell
|
||||
}
|
||||
if v, err := internal.MarshalDashboard(dash); err != nil {
|
||||
return err
|
||||
} else if err := b.Put([]byte(strID), v); err != nil {
|
||||
|
|
|
@ -171,17 +171,14 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
r.Upper, r.Lower = q.Range.Upper, q.Range.Lower
|
||||
}
|
||||
queries[j] = &Query{
|
||||
Command: q.Command,
|
||||
DB: q.DB,
|
||||
RP: q.RP,
|
||||
GroupBys: q.GroupBys,
|
||||
Wheres: q.Wheres,
|
||||
Label: q.Label,
|
||||
Range: r,
|
||||
Command: q.Command,
|
||||
Label: q.Label,
|
||||
Range: r,
|
||||
}
|
||||
}
|
||||
|
||||
cells[i] = &DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
Y: c.Y,
|
||||
W: c.W,
|
||||
|
@ -208,15 +205,11 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
|
||||
cells := make([]chronograf.DashboardCell, len(pb.Cells))
|
||||
for i, c := range pb.Cells {
|
||||
queries := make([]chronograf.Query, len(c.Queries))
|
||||
queries := make([]chronograf.DashboardQuery, len(c.Queries))
|
||||
for j, q := range c.Queries {
|
||||
queries[j] = chronograf.Query{
|
||||
Command: q.Command,
|
||||
DB: q.DB,
|
||||
RP: q.RP,
|
||||
GroupBys: q.GroupBys,
|
||||
Wheres: q.Wheres,
|
||||
Label: q.Label,
|
||||
queries[j] = chronograf.DashboardQuery{
|
||||
Command: q.Command,
|
||||
Label: q.Label,
|
||||
}
|
||||
if q.Range.Upper != q.Range.Lower {
|
||||
queries[j].Range = &chronograf.Range{
|
||||
|
@ -227,6 +220,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
}
|
||||
|
||||
cells[i] = chronograf.DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
Y: c.Y,
|
||||
W: c.W,
|
||||
|
|
|
@ -81,6 +81,7 @@ type DashboardCell struct {
|
|||
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
|
||||
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
|
||||
ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
|
||||
|
@ -224,46 +225,47 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 653 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xd1, 0x6e, 0xd3, 0x4a,
|
||||
0x10, 0xd5, 0xc6, 0x76, 0x12, 0x4f, 0x7b, 0x7b, 0xaf, 0x56, 0x57, 0xb0, 0xe2, 0xc9, 0xb2, 0x40,
|
||||
0x0a, 0x48, 0xf4, 0x81, 0x7e, 0x41, 0x5b, 0x4b, 0x28, 0xd0, 0x96, 0xb2, 0x69, 0xe1, 0x09, 0xa4,
|
||||
0x6d, 0x3a, 0x69, 0x2c, 0x1c, 0xdb, 0xac, 0x6d, 0x52, 0xff, 0x02, 0xe2, 0x0b, 0x78, 0xe0, 0x23,
|
||||
0xf8, 0x15, 0x7e, 0x08, 0xcd, 0x7a, 0xed, 0xb8, 0xa2, 0xa0, 0x3e, 0xf1, 0x36, 0x67, 0x66, 0x73,
|
||||
0x66, 0xe6, 0x9c, 0x71, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
|
||||
0x1f, 0xb7, 0x38, 0xfc, 0x3c, 0x80, 0xe1, 0x2c, 0xab, 0xf4, 0x1c, 0xf9, 0x0e, 0x0c, 0xa6, 0x91,
|
||||
0x60, 0x01, 0x9b, 0x38, 0x72, 0x30, 0x8d, 0x38, 0x07, 0xf7, 0x44, 0xad, 0x50, 0x0c, 0x02, 0x36,
|
||||
0xf1, 0xa5, 0x89, 0x29, 0x77, 0x56, 0xe7, 0x28, 0x9c, 0x26, 0x47, 0x31, 0x7f, 0x00, 0xe3, 0xf3,
|
||||
0x82, 0xd8, 0x56, 0x28, 0x5c, 0x93, 0xef, 0x30, 0xd5, 0x4e, 0x55, 0x51, 0xac, 0x33, 0x7d, 0x29,
|
||||
0xbc, 0xa6, 0xd6, 0x62, 0xfe, 0x1f, 0x38, 0xe7, 0xf2, 0x48, 0x0c, 0x4d, 0x9a, 0x42, 0x2e, 0x60,
|
||||
0x14, 0xe1, 0x42, 0x55, 0x49, 0x29, 0x46, 0x01, 0x9b, 0x8c, 0x65, 0x0b, 0x89, 0xe7, 0x0c, 0x13,
|
||||
0xbc, 0xd2, 0x6a, 0x21, 0xc6, 0x0d, 0x4f, 0x8b, 0xf9, 0x2e, 0xf0, 0x69, 0x5a, 0xe0, 0xbc, 0xd2,
|
||||
0x38, 0xfb, 0x10, 0xe7, 0x6f, 0x50, 0xc7, 0x8b, 0x5a, 0xf8, 0x86, 0xe0, 0x96, 0x0a, 0x75, 0x39,
|
||||
0xc6, 0x52, 0x51, 0x6f, 0x30, 0x54, 0x2d, 0x0c, 0xdf, 0x83, 0x1f, 0xa9, 0x62, 0x79, 0x91, 0x29,
|
||||
0x7d, 0x79, 0x27, 0x39, 0x9e, 0x82, 0x37, 0xc7, 0x24, 0x29, 0x84, 0x13, 0x38, 0x93, 0xad, 0x67,
|
||||
0xf7, 0x77, 0x3b, 0x9d, 0x3b, 0x9e, 0x43, 0x4c, 0x12, 0xd9, 0xbc, 0x0a, 0xbf, 0x32, 0xf8, 0xe7,
|
||||
0x46, 0x81, 0x6f, 0x03, 0xbb, 0x36, 0x3d, 0x3c, 0xc9, 0xae, 0x09, 0xd5, 0x86, 0xdf, 0x93, 0xac,
|
||||
0x26, 0xb4, 0x36, 0x42, 0x7b, 0x92, 0xad, 0x09, 0x2d, 0x8d, 0xbc, 0x9e, 0x64, 0x4b, 0xfe, 0x18,
|
||||
0x46, 0x1f, 0x2b, 0xd4, 0x31, 0x16, 0xc2, 0x33, 0xad, 0xff, 0xdd, 0xb4, 0x7e, 0x5d, 0xa1, 0xae,
|
||||
0x65, 0x5b, 0xa7, 0xb9, 0x8d, 0x35, 0x8d, 0xce, 0x26, 0xa6, 0x5c, 0x49, 0x36, 0x8e, 0x9a, 0x1c,
|
||||
0xc5, 0xe1, 0x17, 0x06, 0xc3, 0x19, 0xea, 0x4f, 0xa8, 0xef, 0xb4, 0x7a, 0xdf, 0x75, 0xe7, 0x0f,
|
||||
0xae, 0xbb, 0xb7, 0xbb, 0xee, 0x6d, 0x5c, 0xff, 0x1f, 0xbc, 0x99, 0x9e, 0x4f, 0x23, 0x33, 0xa1,
|
||||
0x23, 0x1b, 0x10, 0x7e, 0x63, 0x30, 0x3c, 0x52, 0x75, 0x56, 0x95, 0xbd, 0x71, 0x7c, 0x33, 0x4e,
|
||||
0x00, 0x5b, 0xfb, 0x79, 0x9e, 0xc4, 0x73, 0x55, 0xc6, 0x59, 0x6a, 0xa7, 0xea, 0xa7, 0xe8, 0xc5,
|
||||
0x31, 0xaa, 0xa2, 0xd2, 0xb8, 0xc2, 0xb4, 0xb4, 0xf3, 0xf5, 0x53, 0xfc, 0x21, 0x78, 0x87, 0xc6,
|
||||
0x39, 0xd7, 0xc8, 0xb7, 0xb3, 0x91, 0xaf, 0x31, 0xcc, 0x14, 0x69, 0x91, 0xfd, 0xaa, 0xcc, 0x16,
|
||||
0x49, 0xb6, 0x36, 0x13, 0x8f, 0x65, 0x87, 0xc3, 0x1f, 0x0c, 0xdc, 0xbf, 0xe5, 0xe1, 0x36, 0xb0,
|
||||
0xd8, 0x1a, 0xc8, 0xe2, 0xce, 0xd1, 0x51, 0xcf, 0x51, 0x01, 0xa3, 0x5a, 0xab, 0xf4, 0x0a, 0x0b,
|
||||
0x31, 0x0e, 0x9c, 0x89, 0x23, 0x5b, 0x68, 0x2a, 0x89, 0xba, 0xc0, 0xa4, 0x10, 0x7e, 0xe0, 0xd0,
|
||||
0xb9, 0x5b, 0xd8, 0x5d, 0x01, 0xf4, 0xae, 0xe0, 0x3b, 0x03, 0xcf, 0x34, 0xa7, 0xdf, 0x1d, 0x66,
|
||||
0xab, 0x95, 0x4a, 0x2f, 0xad, 0xf4, 0x2d, 0x24, 0x3f, 0xa2, 0x03, 0x2b, 0xfb, 0x20, 0x3a, 0x20,
|
||||
0x2c, 0x4f, 0xad, 0xc8, 0x03, 0x79, 0x4a, 0xaa, 0x3d, 0xd7, 0x59, 0x95, 0x1f, 0xd4, 0x8d, 0xbc,
|
||||
0xbe, 0xec, 0x30, 0xbf, 0x07, 0xc3, 0xb7, 0x4b, 0xd4, 0x76, 0x67, 0x5f, 0x5a, 0x44, 0x47, 0x70,
|
||||
0x44, 0x53, 0xd9, 0x2d, 0x1b, 0xc0, 0x1f, 0x81, 0x27, 0x69, 0x0b, 0xb3, 0xea, 0x0d, 0x81, 0x4c,
|
||||
0x5a, 0x36, 0xd5, 0x70, 0xcf, 0x3e, 0x23, 0x96, 0xf3, 0x3c, 0x47, 0x6d, 0x6f, 0xb7, 0x01, 0x86,
|
||||
0x3b, 0x5b, 0xa3, 0x36, 0x23, 0x3b, 0xb2, 0x01, 0xe1, 0x3b, 0xf0, 0xf7, 0x13, 0xd4, 0xa5, 0xac,
|
||||
0x12, 0xfc, 0xe5, 0xc4, 0x38, 0xb8, 0x2f, 0x66, 0xaf, 0x4e, 0xda, 0x8b, 0xa7, 0x78, 0x73, 0xa7,
|
||||
0x4e, 0xef, 0x4e, 0x69, 0xa1, 0x97, 0x2a, 0x57, 0xd3, 0xc8, 0x18, 0xeb, 0x48, 0x8b, 0xc2, 0x27,
|
||||
0xe0, 0xd2, 0xf7, 0xd0, 0x63, 0x76, 0x7f, 0xf7, 0x2d, 0x5d, 0x0c, 0xcd, 0xbf, 0xf2, 0xde, 0xcf,
|
||||
0x00, 0x00, 0x00, 0xff, 0xff, 0xfa, 0x57, 0xfe, 0xff, 0xa7, 0x05, 0x00, 0x00,
|
||||
// 660 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xdd, 0x6e, 0xd3, 0x4a,
|
||||
0x10, 0xd6, 0xc6, 0x76, 0x7e, 0xa6, 0x3d, 0x3d, 0x47, 0xab, 0x23, 0x58, 0x71, 0x15, 0x59, 0x20,
|
||||
0x05, 0x24, 0x7a, 0x41, 0x9f, 0xa0, 0xad, 0x25, 0x14, 0x68, 0x4b, 0xd9, 0xb4, 0x70, 0x05, 0xd2,
|
||||
0x36, 0x9d, 0x34, 0x16, 0x8e, 0x6d, 0xd6, 0x36, 0xa9, 0x5f, 0x01, 0xf1, 0x0c, 0x3c, 0x00, 0x97,
|
||||
0xbc, 0x0a, 0x2f, 0x84, 0x66, 0x77, 0xed, 0xb8, 0xa2, 0xa0, 0x5e, 0x71, 0x37, 0xdf, 0xcc, 0x66,
|
||||
0x7e, 0xbe, 0xef, 0x73, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
|
||||
0x1f, 0x36, 0x38, 0xfc, 0xdc, 0x83, 0xfe, 0x2c, 0xab, 0xf4, 0x1c, 0xf9, 0x0e, 0xf4, 0xa6, 0x91,
|
||||
0x60, 0x63, 0x36, 0xf1, 0x64, 0x6f, 0x1a, 0x71, 0x0e, 0xfe, 0x89, 0x5a, 0xa1, 0xe8, 0x8d, 0xd9,
|
||||
0x64, 0x24, 0x4d, 0x4c, 0xb9, 0xb3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0x0c, 0xcf,
|
||||
0x0b, 0xea, 0xb6, 0x42, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xaa, 0x8a, 0x62, 0x9d, 0xe9, 0x4b,
|
||||
0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xcb, 0x23, 0xd1, 0x37, 0x69, 0x0a, 0xb9, 0x80,
|
||||
0x41, 0x84, 0x0b, 0x55, 0x25, 0xa5, 0x18, 0x8c, 0xd9, 0x64, 0x28, 0x1b, 0x48, 0x7d, 0xce, 0x30,
|
||||
0xc1, 0x2b, 0xad, 0x16, 0x62, 0x68, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa6, 0x05, 0xce, 0x2b,
|
||||
0x8d, 0xb3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0xbc, 0xa8, 0xc5, 0xc8, 0x34, 0xb8, 0xa5, 0x42, 0x53,
|
||||
0x8e, 0xb1, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x14, 0xa9, 0x62, 0x79, 0x91,
|
||||
0x29, 0x7d, 0x79, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc7, 0x24, 0x29, 0x84, 0x37, 0xf6, 0x26, 0x5b,
|
||||
0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x31, 0x49, 0xa4, 0x7d, 0x15, 0x7e, 0x63, 0xf0,
|
||||
0xcf, 0x8d, 0x02, 0xdf, 0x06, 0x76, 0x6d, 0x66, 0x04, 0x92, 0x5d, 0x13, 0xaa, 0x4d, 0xff, 0x40,
|
||||
0xb2, 0x9a, 0xd0, 0xda, 0x10, 0x1d, 0x48, 0xb6, 0x26, 0xb4, 0x34, 0xf4, 0x06, 0x92, 0x2d, 0xf9,
|
||||
0x63, 0x18, 0x7c, 0xac, 0x50, 0xc7, 0x58, 0x88, 0xc0, 0x8c, 0xfe, 0x77, 0x33, 0xfa, 0x75, 0x85,
|
||||
0xba, 0x96, 0x4d, 0x9d, 0xf6, 0x36, 0xd2, 0x58, 0x9e, 0x4d, 0x4c, 0xb9, 0x92, 0x64, 0x1c, 0xd8,
|
||||
0x1c, 0xc5, 0xee, 0x5e, 0x4b, 0x6e, 0x6f, 0x1a, 0x85, 0x5f, 0x18, 0xf4, 0x67, 0xa8, 0x3f, 0xa1,
|
||||
0xbe, 0x13, 0x15, 0x5d, 0x17, 0x78, 0x7f, 0x70, 0x81, 0x7f, 0xbb, 0x0b, 0x82, 0x8d, 0x0b, 0xfe,
|
||||
0x87, 0x60, 0xa6, 0xe7, 0xd3, 0xc8, 0x6c, 0xec, 0x49, 0x0b, 0xc2, 0xaf, 0x0c, 0xfa, 0x47, 0xaa,
|
||||
0xce, 0xaa, 0xb2, 0xb3, 0x8e, 0xd9, 0x94, 0x8f, 0x61, 0x6b, 0x3f, 0xcf, 0x93, 0x78, 0xae, 0xca,
|
||||
0x38, 0x4b, 0xdd, 0x56, 0xdd, 0x14, 0xbd, 0x38, 0x46, 0x55, 0x54, 0x1a, 0x57, 0x98, 0x96, 0x6e,
|
||||
0xbf, 0x6e, 0x8a, 0x3f, 0x84, 0xe0, 0xd0, 0x28, 0xe9, 0x1b, 0x3a, 0x77, 0x36, 0x74, 0x5a, 0x01,
|
||||
0x4d, 0x91, 0x0e, 0xd9, 0xaf, 0xca, 0x6c, 0x91, 0x64, 0x6b, 0xb3, 0xf1, 0x50, 0xb6, 0x38, 0xfc,
|
||||
0xc1, 0xc0, 0xff, 0x5b, 0x9a, 0x6e, 0x03, 0x8b, 0x9d, 0xa0, 0x2c, 0x6e, 0x15, 0x1e, 0x74, 0x14,
|
||||
0x16, 0x30, 0xa8, 0xb5, 0x4a, 0xaf, 0xb0, 0x10, 0xc3, 0xb1, 0x37, 0xf1, 0x64, 0x03, 0x4d, 0x25,
|
||||
0x51, 0x17, 0x98, 0x14, 0x62, 0x34, 0xf6, 0xc8, 0xfe, 0x0e, 0xb6, 0xae, 0x80, 0x8d, 0x2b, 0xc2,
|
||||
0xef, 0x0c, 0x02, 0x33, 0x9c, 0x7e, 0x77, 0x98, 0xad, 0x56, 0x2a, 0xbd, 0x74, 0xd4, 0x37, 0x90,
|
||||
0xf4, 0x88, 0x0e, 0x1c, 0xed, 0xbd, 0xe8, 0x80, 0xb0, 0x3c, 0x75, 0x24, 0xf7, 0xe4, 0x29, 0xb1,
|
||||
0xf6, 0x5c, 0x67, 0x55, 0x7e, 0x50, 0x5b, 0x7a, 0x47, 0xb2, 0xc5, 0xfc, 0x1e, 0xf4, 0xdf, 0x2e,
|
||||
0x51, 0xbb, 0x9b, 0x47, 0xd2, 0x21, 0x32, 0xc1, 0x11, 0x6d, 0xe5, 0xae, 0xb4, 0x80, 0x3f, 0x82,
|
||||
0x40, 0xd2, 0x15, 0xe6, 0xd4, 0x1b, 0x04, 0x99, 0xb4, 0xb4, 0xd5, 0x70, 0xcf, 0x3d, 0xa3, 0x2e,
|
||||
0xe7, 0x79, 0x8e, 0xda, 0x79, 0xd7, 0x02, 0xd3, 0x3b, 0x5b, 0xa3, 0x36, 0x2b, 0x7b, 0xd2, 0x82,
|
||||
0xf0, 0x1d, 0x8c, 0xf6, 0x13, 0xd4, 0xa5, 0xac, 0x12, 0xfc, 0xc5, 0x62, 0x1c, 0xfc, 0x17, 0xb3,
|
||||
0x57, 0x27, 0x8d, 0xe3, 0x29, 0xde, 0xf8, 0xd4, 0xeb, 0xf8, 0x94, 0x0e, 0x7a, 0xa9, 0x72, 0x35,
|
||||
0x8d, 0x8c, 0xb0, 0x9e, 0x74, 0x28, 0x7c, 0x02, 0x3e, 0x7d, 0x0f, 0x9d, 0xce, 0xfe, 0xef, 0xbe,
|
||||
0xa5, 0x8b, 0xbe, 0xf9, 0x97, 0xde, 0xfb, 0x19, 0x00, 0x00, 0xff, 0xff, 0x93, 0x68, 0x0f, 0xcf,
|
||||
0xb7, 0x05, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ message DashboardCell {
|
|||
repeated Query queries = 5; // Time-series data queries for Dashboard
|
||||
string name = 6; // User-facing name for this Dashboard
|
||||
string type = 7; // Dashboard visualization type
|
||||
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
||||
}
|
||||
|
||||
message Server {
|
||||
|
|
|
@ -14,7 +14,7 @@ func TestServerStore(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Open(); err != nil {
|
||||
if err := c.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestSourceStore(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Open(); err != nil {
|
||||
if err := c.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
|
|
@ -33,7 +33,7 @@ func TestUsersStore_Get(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(); err != nil {
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
@ -79,7 +79,7 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(); err != nil {
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
@ -137,7 +137,7 @@ func TestUsersStore_Delete(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(); err != nil {
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
@ -189,7 +189,7 @@ func TestUsersStore_Update(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(); err != nil {
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
@ -234,7 +234,7 @@ func TestUsersStore_All(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(); err != nil {
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
|
|
@ -121,6 +121,15 @@ type Query struct {
|
|||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
||||
}
|
||||
|
||||
// DashboardQuery includes state for the query builder. This is a transition
|
||||
// struct while we move to the full InfluxQL AST
|
||||
type DashboardQuery struct {
|
||||
Command string `json:"query"` // Command is the query itself
|
||||
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
|
||||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
||||
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
|
||||
}
|
||||
|
||||
// Response is the result of a query against a TimeSeries
|
||||
type Response interface {
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
@ -328,13 +337,14 @@ type Dashboard struct {
|
|||
|
||||
// DashboardCell holds visual and query information for a cell
|
||||
type DashboardCell struct {
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
W int32 `json:"w"`
|
||||
H int32 `json:"h"`
|
||||
Name string `json:"name"`
|
||||
Queries []Query `json:"queries"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"-"`
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
W int32 `json:"w"`
|
||||
H int32 `json:"h"`
|
||||
Name string `json:"name"`
|
||||
Queries []DashboardQuery `json:"queries"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// DashboardsStore is the storage and retrieval of dashboards
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
|
@ -41,7 +42,8 @@ func main() {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := srv.Serve(); err != nil {
|
||||
ctx := context.Background()
|
||||
if err := srv.Serve(ctx); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,411 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/influxdb/influxql"
|
||||
)
|
||||
|
||||
// Convert changes an InfluxQL query to a QueryConfig
|
||||
func Convert(influxQL string) (chronograf.QueryConfig, error) {
|
||||
query, err := influxql.ParseQuery(influxQL)
|
||||
if err != nil {
|
||||
return chronograf.QueryConfig{}, err
|
||||
}
|
||||
|
||||
raw := chronograf.QueryConfig{
|
||||
RawText: query.String(),
|
||||
Fields: []chronograf.Field{},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{},
|
||||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
}
|
||||
qc := chronograf.QueryConfig{
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{},
|
||||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
}
|
||||
|
||||
if len(query.Statements) != 1 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
stmt, ok := query.Statements[0].(*influxql.SelectStatement)
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Query config doesn't support limits
|
||||
if stmt.Limit != 0 || stmt.Offset != 0 || stmt.SLimit != 0 || stmt.SOffset != 0 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Query config doesn't support sorting
|
||||
if len(stmt.SortFields) > 0 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Query config doesn't support fill
|
||||
if stmt.Fill != influxql.NullFill {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Query config doesn't allow SELECT INTO
|
||||
if stmt.Target != nil {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// Query config only allows selecting from one source at a time.
|
||||
if len(stmt.Sources) != 1 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
src := stmt.Sources[0]
|
||||
measurement, ok := src.(*influxql.Measurement)
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
if measurement.Regex != nil {
|
||||
return raw, nil
|
||||
}
|
||||
qc.Database = measurement.Database
|
||||
qc.RetentionPolicy = measurement.RetentionPolicy
|
||||
qc.Measurement = measurement.Name
|
||||
|
||||
for _, dim := range stmt.Dimensions {
|
||||
switch v := dim.Expr.(type) {
|
||||
default:
|
||||
return raw, nil
|
||||
case *influxql.Call:
|
||||
if v.Name != "time" {
|
||||
return raw, nil
|
||||
}
|
||||
// Make sure there is exactly one argument.
|
||||
if len(v.Args) != 1 {
|
||||
return raw, nil
|
||||
}
|
||||
// Ensure the argument is a duration.
|
||||
lit, ok := v.Args[0].(*influxql.DurationLiteral)
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
qc.GroupBy.Time = lit.String()
|
||||
case *influxql.VarRef:
|
||||
qc.GroupBy.Tags = append(qc.GroupBy.Tags, v.Val)
|
||||
}
|
||||
}
|
||||
|
||||
fields := map[string][]string{}
|
||||
for _, fld := range stmt.Fields {
|
||||
switch f := fld.Expr.(type) {
|
||||
case *influxql.Call:
|
||||
// only support certain query config functions
|
||||
if _, ok := supportedFuncs[f.Name]; !ok {
|
||||
return raw, nil
|
||||
}
|
||||
// Query configs only support single argument functions
|
||||
if len(f.Args) != 1 {
|
||||
return raw, nil
|
||||
}
|
||||
ref, ok := f.Args[0].(*influxql.VarRef)
|
||||
// query config only support fields in the function
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
// We only support field strings
|
||||
if ref.Type != influxql.Unknown {
|
||||
return raw, nil
|
||||
}
|
||||
if call, ok := fields[ref.Val]; !ok {
|
||||
fields[ref.Val] = []string{f.Name}
|
||||
} else {
|
||||
fields[ref.Val] = append(call, f.Name)
|
||||
}
|
||||
case *influxql.VarRef:
|
||||
if f.Type != influxql.Unknown {
|
||||
return raw, nil
|
||||
}
|
||||
if _, ok := fields[f.Val]; !ok {
|
||||
fields[f.Val] = []string{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for fld, funcs := range fields {
|
||||
qc.Fields = append(qc.Fields, chronograf.Field{
|
||||
Field: fld,
|
||||
Funcs: funcs,
|
||||
})
|
||||
}
|
||||
|
||||
if stmt.Condition == nil {
|
||||
return qc, nil
|
||||
}
|
||||
|
||||
reduced := influxql.Reduce(stmt.Condition, nil)
|
||||
logic, ok := isTagLogic(reduced)
|
||||
if !ok {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
ops := map[string]bool{}
|
||||
for _, l := range logic {
|
||||
values, ok := qc.Tags[l.Tag]
|
||||
if !ok {
|
||||
values = []string{}
|
||||
}
|
||||
ops[l.Op] = true
|
||||
values = append(values, l.Value)
|
||||
qc.Tags[l.Tag] = values
|
||||
}
|
||||
|
||||
if len(logic) > 0 {
|
||||
if len(ops) != 1 {
|
||||
return raw, nil
|
||||
}
|
||||
if _, ok := ops["=="]; ok {
|
||||
qc.AreTagsAccepted = true
|
||||
}
|
||||
}
|
||||
|
||||
return qc, nil
|
||||
}
|
||||
|
||||
// tagFilter represents a single tag that is filtered by some condition
|
||||
type tagFilter struct {
|
||||
Op string
|
||||
Tag string
|
||||
Value string
|
||||
}
|
||||
|
||||
func isTime(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isTime(p.Expr)
|
||||
} else if ref, ok := exp.(*influxql.VarRef); ok && strings.ToLower(ref.Val) == "time" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNow(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isNow(p.Expr)
|
||||
} else if call, ok := exp.(*influxql.Call); ok && strings.ToLower(call.Name) == "now" && len(call.Args) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDuration(exp influxql.Expr) bool {
|
||||
switch e := exp.(type) {
|
||||
case *influxql.ParenExpr:
|
||||
return isDuration(e.Expr)
|
||||
case *influxql.DurationLiteral, *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPreviousTime(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isPreviousTime(p.Expr)
|
||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
||||
now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now
|
||||
op := bin.Op == influxql.SUB
|
||||
dur := isDuration(bin.LHS) || isDuration(bin.RHS) // either side can be a isDuration
|
||||
return now && op && dur
|
||||
} else if isNow(exp) { // just comparing to now
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTimeRange(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isTimeRange(p.Expr)
|
||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
||||
tm := isTime(bin.LHS) || isTime(bin.RHS) // Either side could be time
|
||||
op := false
|
||||
switch bin.Op {
|
||||
case influxql.LT, influxql.LTE, influxql.GT, influxql.GTE:
|
||||
op = true
|
||||
}
|
||||
prev := isPreviousTime(bin.LHS) || isPreviousTime(bin.RHS)
|
||||
return tm && op && prev
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasTimeRange(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return hasTimeRange(p.Expr)
|
||||
} else if isTimeRange(exp) {
|
||||
return true
|
||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
||||
return isTimeRange(bin.LHS) || isTimeRange(bin.RHS)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isTagLogic(p.Expr)
|
||||
}
|
||||
|
||||
if isTimeRange(exp) {
|
||||
return nil, true
|
||||
} else if tf, ok := isTagFilter(exp); ok {
|
||||
return []tagFilter{tf}, true
|
||||
}
|
||||
|
||||
bin, ok := exp.(*influxql.BinaryExpr)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
lhs, lhsOK := isTagFilter(bin.LHS)
|
||||
rhs, rhsOK := isTagFilter(bin.RHS)
|
||||
|
||||
if lhsOK && rhsOK && lhs.Tag == rhs.Tag && lhs.Op == rhs.Op && bin.Op == influxql.OR {
|
||||
return []tagFilter{lhs, rhs}, true
|
||||
}
|
||||
|
||||
if bin.Op != influxql.AND {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tm := isTimeRange(bin.LHS) || isTimeRange(bin.RHS)
|
||||
tf := lhsOK || rhsOK
|
||||
if tm && tf {
|
||||
if lhsOK {
|
||||
return []tagFilter{lhs}, true
|
||||
}
|
||||
return []tagFilter{rhs}, true
|
||||
}
|
||||
|
||||
tlLHS, lhsOK := isTagLogic(bin.LHS)
|
||||
tlRHS, rhsOK := isTagLogic(bin.RHS)
|
||||
if lhsOK && rhsOK {
|
||||
ops := map[string]bool{} // there must only be one kind of ops
|
||||
for _, tf := range tlLHS {
|
||||
ops[tf.Op] = true
|
||||
}
|
||||
for _, tf := range tlRHS {
|
||||
ops[tf.Op] = true
|
||||
}
|
||||
if len(ops) > 1 {
|
||||
return nil, false
|
||||
}
|
||||
return append(tlLHS, tlRHS...), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func hasTagFilter(exp influxql.Expr) bool {
|
||||
if _, ok := isTagFilter(exp); ok {
|
||||
return true
|
||||
} else if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return hasTagFilter(p.Expr)
|
||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
||||
or := bin.Op == influxql.OR
|
||||
and := bin.Op == influxql.AND
|
||||
op := or || and
|
||||
return op && (hasTagFilter(bin.LHS) || hasTagFilter(bin.RHS))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func singleTagFilter(exp influxql.Expr) (tagFilter, bool) {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return singleTagFilter(p.Expr)
|
||||
} else if tf, ok := isTagFilter(exp); ok {
|
||||
return tf, true
|
||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok && bin.Op == influxql.OR {
|
||||
lhs, lhsOK := singleTagFilter(bin.LHS)
|
||||
rhs, rhsOK := singleTagFilter(bin.RHS)
|
||||
if lhsOK && rhsOK && lhs.Op == rhs.Op && lhs.Tag == rhs.Tag {
|
||||
return lhs, true
|
||||
}
|
||||
}
|
||||
return tagFilter{}, false
|
||||
}
|
||||
|
||||
func isVarRef(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isVarRef(p.Expr)
|
||||
} else if _, ok := exp.(*influxql.VarRef); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isString(exp influxql.Expr) bool {
|
||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
||||
return isString(p.Expr)
|
||||
} else if _, ok := exp.(*influxql.StringLiteral); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTagFilter(exp influxql.Expr) (tagFilter, bool) {
|
||||
switch expr := exp.(type) {
|
||||
default:
|
||||
return tagFilter{}, false
|
||||
case *influxql.ParenExpr:
|
||||
return isTagFilter(expr.Expr)
|
||||
case *influxql.BinaryExpr:
|
||||
var Op string
|
||||
if expr.Op == influxql.EQ {
|
||||
Op = "=="
|
||||
} else if expr.Op == influxql.NEQ {
|
||||
Op = "!="
|
||||
} else {
|
||||
return tagFilter{}, false
|
||||
}
|
||||
|
||||
hasValue := isString(expr.LHS) || isString(expr.RHS)
|
||||
hasTag := isVarRef(expr.LHS) || isVarRef(expr.RHS)
|
||||
if !(hasValue && hasTag) {
|
||||
return tagFilter{}, false
|
||||
}
|
||||
|
||||
value := ""
|
||||
tag := ""
|
||||
// Either tag op value or value op tag
|
||||
if isVarRef(expr.LHS) {
|
||||
t, _ := expr.LHS.(*influxql.VarRef)
|
||||
tag = t.Val
|
||||
v, _ := expr.RHS.(*influxql.StringLiteral)
|
||||
value = v.Val
|
||||
} else {
|
||||
t, _ := expr.RHS.(*influxql.VarRef)
|
||||
tag = t.Val
|
||||
v, _ := expr.LHS.(*influxql.StringLiteral)
|
||||
value = v.Val
|
||||
}
|
||||
|
||||
return tagFilter{
|
||||
Op: Op,
|
||||
Tag: tag,
|
||||
Value: value,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
var supportedFuncs = map[string]bool{
|
||||
"mean": true,
|
||||
"median": true,
|
||||
"count": true,
|
||||
"min": true,
|
||||
"max": true,
|
||||
"sum": true,
|
||||
"first": true,
|
||||
"last": true,
|
||||
"spread": true,
|
||||
"stddev": true,
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -18,25 +20,50 @@ const (
|
|||
)
|
||||
|
||||
type dashboardLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
Cells string `json:"cells"` // Cells link to the cells endpoint
|
||||
}
|
||||
|
||||
type dashboardCellLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
type dashboardCellResponse struct {
|
||||
chronograf.DashboardCell
|
||||
Links dashboardCellLinks `json:"links"`
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
chronograf.Dashboard
|
||||
Links dashboardLinks `json:"links"`
|
||||
ID chronograf.DashboardID `json:"id"`
|
||||
Cells []dashboardCellResponse `json:"cells"`
|
||||
Name string `json:"name"`
|
||||
Links dashboardLinks `json:"links"`
|
||||
}
|
||||
|
||||
type getDashboardsResponse struct {
|
||||
Dashboards []dashboardResponse `json:"dashboards"`
|
||||
Dashboards []*dashboardResponse `json:"dashboards"`
|
||||
}
|
||||
|
||||
func newDashboardResponse(d chronograf.Dashboard) dashboardResponse {
|
||||
func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
DashboardDefaults(&d)
|
||||
return dashboardResponse{
|
||||
Dashboard: d,
|
||||
AddQueryConfigs(&d)
|
||||
cells := make([]dashboardCellResponse, len(d.Cells))
|
||||
for i, cell := range d.Cells {
|
||||
cells[i] = dashboardCellResponse{
|
||||
DashboardCell: cell,
|
||||
Links: dashboardCellLinks{
|
||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, d.ID, cell.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
return &dashboardResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Cells: cells,
|
||||
Links: dashboardLinks{
|
||||
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
||||
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
||||
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +78,7 @@ func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
res := getDashboardsResponse{
|
||||
Dashboards: []dashboardResponse{},
|
||||
Dashboards: []*dashboardResponse{},
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
|
@ -228,6 +255,15 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ValidDashboardCellRequest verifies that the dashboard cells have a query
|
||||
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
||||
if len(c.Queries) == 0 {
|
||||
return fmt.Errorf("query required")
|
||||
}
|
||||
CorrectWidthHeight(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DashboardDefaults updates the dashboard with the default values
|
||||
// if none are specified
|
||||
func DashboardDefaults(d *chronograf.Dashboard) {
|
||||
|
@ -247,3 +283,224 @@ func CorrectWidthHeight(c *chronograf.DashboardCell) {
|
|||
c.H = DefaultHeight
|
||||
}
|
||||
}
|
||||
|
||||
// AddQueryConfigs updates all the celsl in the dashboard to have query config
|
||||
// objects corresponding to their influxql queries.
|
||||
func AddQueryConfigs(d *chronograf.Dashboard) {
|
||||
for i, c := range d.Cells {
|
||||
AddQueryConfig(&c)
|
||||
d.Cells[i] = c
|
||||
}
|
||||
}
|
||||
|
||||
// AddQueryConfig updates a cell by converting InfluxQL into queryconfigs
|
||||
// If influxql cannot be represented by a full query config, then, the
|
||||
// query config's raw text is set to the command.
|
||||
func AddQueryConfig(c *chronograf.DashboardCell) {
|
||||
for i, q := range c.Queries {
|
||||
qc, err := influx.Convert(q.Command)
|
||||
if err == nil {
|
||||
q.QueryConfig = qc
|
||||
c.Queries[i] = q
|
||||
} else {
|
||||
q.QueryConfig = chronograf.QueryConfig{
|
||||
RawText: q.Command,
|
||||
Fields: []chronograf.Field{},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{},
|
||||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
}
|
||||
c.Queries[i] = q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardCells returns all cells from a dashboard within the store
|
||||
func (s *Service) DashboardCells(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
boards := newDashboardResponse(e)
|
||||
cells := boards.Cells
|
||||
encodeJSON(w, http.StatusOK, cells, s.Logger)
|
||||
}
|
||||
|
||||
// NewDashboardCell adds a cell to an existing dashboard
|
||||
func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
var cell chronograf.DashboardCell
|
||||
if err := json.NewDecoder(r.Body).Decode(&cell); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidDashboardCellRequest(&cell); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ids := uuid.V4{}
|
||||
cid, err := ids.Generate()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error creating cell ID of dashboard %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
cell.ID = cid
|
||||
|
||||
dash.Cells = append(dash.Cells, cell)
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
boards := newDashboardResponse(dash)
|
||||
for _, cell := range boards.Cells {
|
||||
if cell.ID == cid {
|
||||
encodeJSON(w, http.StatusOK, cell, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardCellID adds a cell to an existing dashboard
|
||||
func (s *Service) DashboardCellID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
boards := newDashboardResponse(dash)
|
||||
cid := httprouter.GetParamFromContext(ctx, "cid")
|
||||
for _, cell := range boards.Cells {
|
||||
if cell.ID == cid {
|
||||
encodeJSON(w, http.StatusOK, cell, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
notFound(w, id, s.Logger)
|
||||
}
|
||||
|
||||
// RemoveDashboardCell adds a cell to an existing dashboard
|
||||
func (s *Service) RemoveDashboardCell(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
cid := httprouter.GetParamFromContext(ctx, "cid")
|
||||
cellid := -1
|
||||
for i, cell := range dash.Cells {
|
||||
if cell.ID == cid {
|
||||
cellid = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if cellid == -1 {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dash.Cells = append(dash.Cells[:cellid], dash.Cells[cellid+1:]...)
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ReplaceDashboardCell adds a cell to an existing dashboard
|
||||
func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := paramID("id", r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||
if err != nil {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
cid := httprouter.GetParamFromContext(ctx, "cid")
|
||||
cellid := -1
|
||||
for i, cell := range dash.Cells {
|
||||
if cell.ID == cid {
|
||||
cellid = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if cellid == -1 {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var cell chronograf.DashboardCell
|
||||
if err := json.NewDecoder(r.Body).Decode(&cell); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidDashboardCellRequest(&cell); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
cell.ID = cid
|
||||
|
||||
dash.Cells[cellid] = cell
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
boards := newDashboardResponse(dash)
|
||||
for _, cell := range boards.Cells {
|
||||
if cell.ID == cid {
|
||||
encodeJSON(w, http.StatusOK, cell, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
|
@ -156,7 +156,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
|
@ -169,7 +169,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
|
@ -178,7 +178,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
|
@ -194,7 +194,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{},
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -203,7 +203,7 @@ func TestValidDashboardRequest(t *testing.T) {
|
|||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{},
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -236,64 +236,100 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
d chronograf.Dashboard
|
||||
want dashboardResponse
|
||||
want *dashboardResponse
|
||||
}{
|
||||
{
|
||||
name: "Updates all cell widths/heights",
|
||||
name: "creates a dashboard response",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
ID: "a",
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
ID: "b",
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: dashboardResponse{
|
||||
Dashboard: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
want: &dashboardResponse{
|
||||
Cells: []dashboardCellResponse{
|
||||
dashboardCellResponse{
|
||||
Links: dashboardCellLinks{
|
||||
Self: "/chronograf/v1/dashboards/0/cells/a",
|
||||
},
|
||||
DashboardCell: chronograf.DashboardCell{
|
||||
ID: "a",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'",
|
||||
QueryConfig: chronograf.QueryConfig{
|
||||
RawText: "SELECT donors FROM hill_valley_preservation_society WHERE time > '1985-10-25 08:00:00'",
|
||||
Fields: []chronograf.Field{},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{},
|
||||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
AreTagsAccepted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
},
|
||||
dashboardCellResponse{
|
||||
Links: dashboardCellLinks{
|
||||
Self: "/chronograf/v1/dashboards/0/cells/b",
|
||||
},
|
||||
DashboardCell: chronograf.DashboardCell{
|
||||
ID: "b",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m",
|
||||
QueryConfig: chronograf.QueryConfig{
|
||||
Measurement: "grays_sports_alamanc",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Field: "winning_horses",
|
||||
Funcs: []string{},
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{},
|
||||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
AreTagsAccepted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Links: dashboardLinks{
|
||||
Self: "/chronograf/v1/dashboards/0",
|
||||
Self: "/chronograf/v1/dashboards/0",
|
||||
Cells: "/chronograf/v1/dashboards/0/cells",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. newDashboardResponse() = %v, want %v", tt.name, got, tt.want)
|
||||
t.Errorf("%q. newDashboardResponse() = \n%+v\n\n, want\n\n%+v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,6 +130,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
|
||||
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
|
||||
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
|
||||
// Dashboard Cells
|
||||
router.GET("/chronograf/v1/dashboards/:id/cells", service.DashboardCells)
|
||||
router.POST("/chronograf/v1/dashboards/:id/cells", service.NewDashboardCell)
|
||||
|
||||
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
|
||||
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
||||
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
||||
|
||||
var authRoutes AuthRoutes
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
@ -176,9 +177,9 @@ func (s *Server) NewListener() (net.Listener, error) {
|
|||
}
|
||||
|
||||
// Serve starts and runs the chronograf server
|
||||
func (s *Server) Serve() error {
|
||||
func (s *Server) Serve(ctx context.Context) error {
|
||||
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
||||
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
|
||||
service := openService(ctx, s.BoltPath, s.CannedPath, logger, s.useAuth())
|
||||
basepath = s.Basepath
|
||||
|
||||
providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){}
|
||||
|
@ -254,10 +255,10 @@ func (s *Server) Serve() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service {
|
||||
func openService(ctx context.Context, boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service {
|
||||
db := bolt.NewClient()
|
||||
db.Path = boltPath
|
||||
if err := db.Open(); err != nil {
|
||||
if err := db.Open(ctx); err != nil {
|
||||
logger.
|
||||
WithField("component", "boltstore").
|
||||
Error("Unable to open boltdb; is there a chronograf already running? ", err)
|
||||
|
|
|
@ -3030,6 +3030,10 @@
|
|||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"description": "Cell name",
|
||||
"type": "string"
|
||||
},
|
||||
"queries": {
|
||||
"description": "Time-series data queries for Cell.",
|
||||
"type": "array",
|
||||
|
@ -3039,8 +3043,7 @@
|
|||
},
|
||||
"type": {
|
||||
"description": "Cell visualization type",
|
||||
"type": "string",
|
||||
"format": "uuid4"
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
@ -3061,7 +3064,7 @@
|
|||
"LayoutQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"query"
|
||||
"query",
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
|
@ -3117,6 +3120,86 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"DashboardQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"query"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"description": "Optional Y-axis user-facing label for this query",
|
||||
"type": "string"
|
||||
},
|
||||
"range": {
|
||||
"description": "Optional default range of the Y-axis",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"upper",
|
||||
"lower"
|
||||
],
|
||||
"properties": {
|
||||
"upper": {
|
||||
"description": "Upper bound of the display range of the Y-axis",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"lower": {
|
||||
"description": "Lower bound of the display range of the Y-axis",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"queryConfig": {
|
||||
"$ref": "#/definitions/QueryConfig"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"id": 4,
|
||||
"cells": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 4,
|
||||
"name": "",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
|
||||
"label": "%",
|
||||
"queryConfig": {
|
||||
"database": "",
|
||||
"measurement": "cpu",
|
||||
"retentionPolicy": "",
|
||||
"fields": [
|
||||
{
|
||||
"field": "usage_user",
|
||||
"funcs": [
|
||||
"mean"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tags": {},
|
||||
"groupBy": {
|
||||
"time": "",
|
||||
"tags": []
|
||||
},
|
||||
"areTagsAccepted": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "line"
|
||||
}
|
||||
],
|
||||
"name": "dashboard name",
|
||||
"links": {
|
||||
"self": "/chronograf/v1/dashboards/4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Dashboards": {
|
||||
"description": "a list of dashboards",
|
||||
"type": "object",
|
||||
|
@ -3171,11 +3254,15 @@
|
|||
"minimum": 1,
|
||||
"default": 4
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of Cell in the Dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"queries": {
|
||||
"description": "Time-series data queries for Cell.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/LayoutQuery"
|
||||
"$ref": "#/definitions/DashboardQuery"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
|
@ -3189,6 +3276,16 @@
|
|||
"line-stepplot"
|
||||
],
|
||||
"default": "line"
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "string",
|
||||
"description": "Self link mapping to this resource",
|
||||
"format": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3220,6 +3317,7 @@
|
|||
"queries": [
|
||||
{
|
||||
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
|
||||
"db": "telegraf",
|
||||
"label": "%"
|
||||
}
|
||||
],
|
||||
|
@ -3234,6 +3332,7 @@
|
|||
"queries": [
|
||||
{
|
||||
"query": "SELECT mean(\"usage_system\") AS \"usage_system\" FROM \"cpu\"",
|
||||
"db": "telegraf",
|
||||
"label": "%"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
updateDashboardCells,
|
||||
editCell,
|
||||
renameCell,
|
||||
syncDashboardCell,
|
||||
} from 'src/dashboards/actions'
|
||||
|
||||
const noopAction = () => {
|
||||
|
@ -20,6 +21,16 @@ const timeRange = timeRanges[1];
|
|||
const d1 = {id: 1, cells: [], name: "d1"}
|
||||
const d2 = {id: 2, cells: [], name: "d2"}
|
||||
const dashboards = [d1, d2]
|
||||
const c1 = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
id: 1,
|
||||
isEditing: false,
|
||||
name: "Gigawatts",
|
||||
}
|
||||
const cells = [c1]
|
||||
|
||||
describe('DataExplorer.Reducers.UI', () => {
|
||||
it('can load the dashboards', () => {
|
||||
|
@ -74,19 +85,7 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
})
|
||||
|
||||
it('can edit cell', () => {
|
||||
const dash = {
|
||||
id: 1,
|
||||
cells: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
id: 1,
|
||||
isEditing: false,
|
||||
name: "Gigawatts",
|
||||
}],
|
||||
}
|
||||
|
||||
const dash = {...d1, cells}
|
||||
state = {
|
||||
dashboard: dash,
|
||||
dashboards: [dash],
|
||||
|
@ -97,20 +96,27 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
expect(actual.dashboard.cells[0].isEditing).to.equal(true)
|
||||
})
|
||||
|
||||
it('can rename cells', () => {
|
||||
const dash = {
|
||||
id: 1,
|
||||
cells: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 4,
|
||||
id: 1,
|
||||
isEditing: true,
|
||||
name: "Gigawatts",
|
||||
}],
|
||||
it('can sync a cell', () => {
|
||||
const newCellName = 'watts is kinda cool'
|
||||
const newCell = {
|
||||
x: c1.x,
|
||||
y: c1.y,
|
||||
name: newCellName
|
||||
}
|
||||
const dash = {...d1, cells: [c1]}
|
||||
state = {
|
||||
dashboard: dash,
|
||||
dashboards: [dash],
|
||||
}
|
||||
|
||||
const actual = reducer(state, syncDashboardCell(newCell))
|
||||
expect(actual.dashboards[0].cells[0].name).to.equal(newCellName)
|
||||
expect(actual.dashboard.cells[0].name).to.equal(newCellName)
|
||||
})
|
||||
|
||||
it('can rename cells', () => {
|
||||
const c2 = {...c1, isEditing: true}
|
||||
const dash = {...d1, cells: [c2]}
|
||||
state = {
|
||||
dashboard: dash,
|
||||
dashboards: [dash],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const ConfirmButtons = ({onConfirm, item, onCancel}) => (
|
||||
<div>
|
||||
<div className="confirm-buttons">
|
||||
<button
|
||||
className="btn btn-xs btn-info"
|
||||
onClick={() => onCancel(item)}
|
||||
|
@ -24,7 +24,7 @@ const {
|
|||
|
||||
ConfirmButtons.propTypes = {
|
||||
onConfirm: func.isRequired,
|
||||
item: shape({}).isRequired,
|
||||
item: shape({}),
|
||||
onCancel: func.isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
getDashboards as getDashboardsAJAX,
|
||||
updateDashboard as updateDashboardAJAX,
|
||||
updateDashboardCell as updateDashboardCellAJAX,
|
||||
} from 'src/dashboards/apis'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
|
@ -46,6 +47,13 @@ export const updateDashboardCells = (cells) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const syncDashboardCell = (cell) => ({
|
||||
type: 'SYNC_DASHBOARD_CELL',
|
||||
payload: {
|
||||
cell,
|
||||
},
|
||||
})
|
||||
|
||||
export const editCell = (x, y, isEditing) => ({
|
||||
type: 'EDIT_CELL',
|
||||
// x and y coords are used as a alternative to cell ids, which are not
|
||||
|
@ -82,3 +90,10 @@ export const putDashboard = () => (dispatch, getState) => {
|
|||
dispatch(updateDashboard(data))
|
||||
})
|
||||
}
|
||||
|
||||
export const updateDashboardCell = (cell) => (dispatch) => {
|
||||
return updateDashboardCellAJAX(cell)
|
||||
.then(({data}) => {
|
||||
dispatch(syncDashboardCell(data))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,6 +15,14 @@ export function updateDashboard(dashboard) {
|
|||
});
|
||||
}
|
||||
|
||||
export function updateDashboardCell(cell) {
|
||||
return AJAX({
|
||||
method: 'PUT',
|
||||
url: cell.links.self,
|
||||
data: cell,
|
||||
})
|
||||
}
|
||||
|
||||
export const createDashboard = async (dashboard) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
|
||||
import QueryBuilder from 'src/data_explorer/components/QueryBuilder'
|
||||
import Visualization from 'src/data_explorer/components/Visualization'
|
||||
import OverlayControls from 'src/dashboards/components/OverlayControls'
|
||||
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||
|
||||
import {buildSelectStatement} from 'src/data_explorer/utils/influxql/select'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
|
||||
class CellEditorOverlay extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.queryStateReducer = ::this.queryStateReducer
|
||||
|
||||
this.handleAddQuery = ::this.handleAddQuery
|
||||
this.handleDeleteQuery = ::this.handleDeleteQuery
|
||||
|
||||
this.handleSaveCell = ::this.handleSaveCell
|
||||
|
||||
this.handleSelectGraphType = ::this.handleSelectGraphType
|
||||
this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
|
||||
|
||||
const {cell: {name, type, queries}} = props
|
||||
const queriesWorkingDraft = _.cloneDeep(queries.map(({queryConfig}) => queryConfig))
|
||||
|
||||
this.state = {
|
||||
cellWorkingName: name,
|
||||
cellWorkingType: type,
|
||||
queriesWorkingDraft,
|
||||
activeQueryIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
queryStateReducer(queryModifier) {
|
||||
return (queryID, payload) => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
const query = queriesWorkingDraft.find((q) => q.id === queryID)
|
||||
|
||||
const nextQuery = queryModifier(query, payload)
|
||||
|
||||
const nextQueries = queriesWorkingDraft.map((q) => q.id === query.id ? nextQuery : q)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
}
|
||||
|
||||
handleAddQuery(options) {
|
||||
const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
|
||||
const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
|
||||
handleDeleteQuery(index) {
|
||||
const nextQueries = this.state.queriesWorkingDraft.filter((__, i) => i !== index)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
|
||||
handleSaveCell() {
|
||||
const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state
|
||||
const {cell} = this.props
|
||||
|
||||
const newCell = _.cloneDeep(cell)
|
||||
newCell.name = cellWorkingName
|
||||
newCell.type = cellWorkingType
|
||||
newCell.queries = queriesWorkingDraft.map((q) => {
|
||||
const query = q.rawText || buildSelectStatement(q)
|
||||
const label = `${q.measurement}.${q.fields[0].field}`
|
||||
|
||||
return {
|
||||
queryConfig: q,
|
||||
query,
|
||||
label,
|
||||
}
|
||||
})
|
||||
|
||||
this.props.onSave(newCell)
|
||||
}
|
||||
|
||||
handleSelectGraphType(graphType) {
|
||||
this.setState({cellWorkingType: graphType})
|
||||
}
|
||||
|
||||
handleSetActiveQueryIndex(activeQueryIndex) {
|
||||
this.setState({activeQueryIndex})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {onCancel, autoRefresh, timeRange} = this.props
|
||||
const {activeQueryIndex, cellWorkingType, queriesWorkingDraft} = this.state
|
||||
const queryActions = {
|
||||
addQuery: this.handleAddQuery,
|
||||
..._.mapValues(queryModifiers, (qm) => this.queryStateReducer(qm)),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="data-explorer overlay-technology">
|
||||
<ResizeContainer>
|
||||
<Visualization
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
activeQueryIndex={0}
|
||||
cellType={cellWorkingType}
|
||||
/>
|
||||
<ResizeBottom>
|
||||
<OverlayControls
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onCancel={onCancel}
|
||||
onSave={this.handleSaveCell}
|
||||
/>
|
||||
<QueryBuilder
|
||||
queries={queriesWorkingDraft}
|
||||
actions={queryActions}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
setActiveQueryIndex={this.handleSetActiveQueryIndex}
|
||||
onDeleteQuery={this.handleDeleteQuery}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
/>
|
||||
</ResizeBottom>
|
||||
</ResizeContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
CellEditorOverlay.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
cell: shape({}).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
}
|
||||
|
||||
export default CellEditorOverlay
|
|
@ -12,6 +12,7 @@ const Dashboard = ({
|
|||
onEditCell,
|
||||
onRenameCell,
|
||||
onUpdateCell,
|
||||
onSummonOverlayTechnologies,
|
||||
source,
|
||||
autoRefresh,
|
||||
timeRange,
|
||||
|
@ -24,20 +25,26 @@ const Dashboard = ({
|
|||
<div className={classnames({'page-contents': true, 'presentation-mode': inPresentationMode})}>
|
||||
<div className={classnames('container-fluid full-width dashboard', {'dashboard-edit': isEditMode})}>
|
||||
{isEditMode ? <Visualizations/> : null}
|
||||
{Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell)}
|
||||
{Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell, onSummonOverlayTechnologies)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell) => {
|
||||
Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell, onSummonOverlayTechnologies) => {
|
||||
const cells = dashboard.cells.map((cell, i) => {
|
||||
i = `${i}`
|
||||
const dashboardCell = {...cell, i}
|
||||
dashboardCell.queries.forEach((q) => {
|
||||
q.text = q.query;
|
||||
q.database = q.db;
|
||||
});
|
||||
dashboardCell.queries = dashboardCell.queries.map(({label, query, queryConfig, db}) =>
|
||||
({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
database: db,
|
||||
text: query,
|
||||
})
|
||||
)
|
||||
return dashboardCell;
|
||||
})
|
||||
|
||||
|
@ -51,6 +58,7 @@ Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositi
|
|||
onEditCell={onEditCell}
|
||||
onRenameCell={onRenameCell}
|
||||
onUpdateCell={onUpdateCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -71,6 +79,7 @@ Dashboard.propTypes = {
|
|||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
onSummonOverlayTechnologies: func,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string,
|
||||
|
|
|
@ -52,7 +52,7 @@ const DashboardHeader = ({
|
|||
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
<AutoRefreshDropdown onChoose={handleChooseAutoRefresh} selected={autoRefresh} iconName="refresh" />
|
||||
<TimeRangeDropdown onChooseTimeRange={handleChooseTimeRange} selected={timeRange.inputValue} />
|
||||
<TimeRangeDropdown onChooseTimeRange={handleChooseTimeRange} selected={timeRange} />
|
||||
<div className="btn btn-info btn-sm" onClick={handleClickPresentationButton}>
|
||||
<span className="icon expand-a" style={{margin: 0}}></span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
|
||||
|
||||
import graphTypes from 'hson!shared/data/graphTypes.hson'
|
||||
|
||||
const OverlayControls = (props) => {
|
||||
const {onCancel, onSave, selectedGraphType, onSelectGraphType} = props
|
||||
return (
|
||||
<div className="overlay-controls">
|
||||
<h3 className="overlay--graph-name">Graph Editor</h3>
|
||||
<div className="overlay-controls--right">
|
||||
<p>Visualization Type:</p>
|
||||
<ul className="toggle toggle-sm">
|
||||
{graphTypes.map(graphType =>
|
||||
<li
|
||||
key={graphType.type}
|
||||
className={classnames('toggle-btn', {active: graphType.type === selectedGraphType})}
|
||||
onClick={() => onSelectGraphType(graphType.type)}
|
||||
>
|
||||
{graphType.menuOption}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ConfirmButtons onCancel={onCancel} onConfirm={onSave} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
OverlayControls.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
selectedGraphType: string.isRequired,
|
||||
onSelectGraphType: func.isRequired,
|
||||
}
|
||||
|
||||
export default OverlayControls
|
|
@ -7,6 +7,7 @@ export const EMPTY_DASHBOARD = {
|
|||
y: 0,
|
||||
queries: [],
|
||||
name: 'Loading...',
|
||||
type: 'single-stat',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -3,13 +3,14 @@ import {Link} from 'react-router'
|
|||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
|
||||
import Header from 'src/dashboards/components/DashboardHeader'
|
||||
import EditHeader from 'src/dashboards/components/DashboardHeaderEdit'
|
||||
import Dashboard from 'src/dashboards/components/Dashboard'
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
|
||||
|
||||
import * as dashboardActionCreators from 'src/dashboards/actions'
|
||||
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
|
||||
const {
|
||||
|
@ -53,6 +54,7 @@ const DashboardPage = React.createClass({
|
|||
id: number.isRequired,
|
||||
cells: arrayOf(shape({})).isRequired,
|
||||
}).isRequired,
|
||||
handleChooseAutoRefresh: func.isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
inPresentationMode: bool.isRequired,
|
||||
|
@ -60,6 +62,25 @@ const DashboardPage = React.createClass({
|
|||
handleClickPresentationButton: func,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
self: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {source: this.props.source};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedCell: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
params: {dashboardID},
|
||||
|
@ -85,9 +106,21 @@ const DashboardPage = React.createClass({
|
|||
setEditMode(nextPathname.includes('/edit'))
|
||||
},
|
||||
|
||||
handleDismissOverlay() {
|
||||
this.setState({selectedCell: null})
|
||||
},
|
||||
|
||||
handleSaveEditedCell(newCell) {
|
||||
this.props.dashboardActions.updateDashboardCell(newCell)
|
||||
.then(this.handleDismissOverlay)
|
||||
},
|
||||
|
||||
handleSummonOverlayTechnologies(cell) {
|
||||
this.setState({selectedCell: cell})
|
||||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find((range) => range.queryValue === lower);
|
||||
this.props.dashboardActions.setTimeRange(timeRange)
|
||||
this.props.dashboardActions.setTimeRange({lower, upper: null})
|
||||
},
|
||||
|
||||
handleUpdatePosition(cells) {
|
||||
|
@ -124,17 +157,34 @@ const DashboardPage = React.createClass({
|
|||
isEditMode,
|
||||
handleClickPresentationButton,
|
||||
source,
|
||||
handleChooseAutoRefresh,
|
||||
autoRefresh,
|
||||
timeRange,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
selectedCell,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
{
|
||||
selectedCell && selectedCell.queries.length ?
|
||||
<CellEditorOverlay
|
||||
cell={selectedCell}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
onCancel={this.handleDismissOverlay}
|
||||
onSave={this.handleSaveEditedCell}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
{
|
||||
isEditMode ?
|
||||
<EditHeader dashboard={dashboard} onSave={() => {}} /> :
|
||||
<Header
|
||||
buttonText={dashboard ? dashboard.name : ''}
|
||||
handleChooseAutoRefresh={handleChooseAutoRefresh}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
handleChooseTimeRange={this.handleChooseTimeRange}
|
||||
|
@ -166,6 +216,7 @@ const DashboardPage = React.createClass({
|
|||
onEditCell={this.handleEditCell}
|
||||
onRenameCell={this.handleChangeCellName}
|
||||
onUpdateCell={this.handleUpdateCell}
|
||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -197,6 +248,7 @@ const mapStateToProps = (state) => {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
||||
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
||||
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
|
||||
})
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import _ from 'lodash'
|
||||
import {EMPTY_DASHBOARD} from 'src/dashboards/constants'
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
|
||||
const {lower, upper} = timeRanges[1]
|
||||
|
||||
const initialState = {
|
||||
dashboards: [],
|
||||
dashboard: EMPTY_DASHBOARD,
|
||||
timeRange: timeRanges[1],
|
||||
timeRange: {lower, upper},
|
||||
isEditMode: false,
|
||||
};
|
||||
|
||||
|
@ -92,6 +94,23 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'SYNC_DASHBOARD_CELL': {
|
||||
const {cell} = action.payload
|
||||
const {dashboard} = state
|
||||
|
||||
const newDashboard = {
|
||||
...dashboard,
|
||||
cells: dashboard.cells.map((c) => c.x === cell.x && c.y === cell.y ? cell : c),
|
||||
}
|
||||
|
||||
const newState = {
|
||||
dashboard: newDashboard,
|
||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'RENAME_CELL': {
|
||||
const {x, y, name} = action.payload
|
||||
const {dashboard} = state
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import uuid from 'node-uuid';
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
export function addQuery(options = {}) {
|
||||
return {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTabItem from './QueryTabItem';
|
||||
import SimpleDropdown from 'src/shared/components/SimpleDropdown';
|
||||
|
||||
import * as viewActions from '../actions/view';
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
node,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
@ -27,7 +26,6 @@ const QueryBuilder = React.createClass({
|
|||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
addQuery: func.isRequired,
|
||||
deleteQuery: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
|
@ -35,32 +33,34 @@ const QueryBuilder = React.createClass({
|
|||
}).isRequired,
|
||||
height: string,
|
||||
top: string,
|
||||
setActiveQuery: func.isRequired,
|
||||
activeQueryID: string,
|
||||
setActiveQueryIndex: func.isRequired,
|
||||
onDeleteQuery: func.isRequired,
|
||||
activeQueryIndex: number,
|
||||
children: node,
|
||||
},
|
||||
|
||||
handleSetActiveQuery(query) {
|
||||
this.props.setActiveQuery(query.id);
|
||||
handleSetActiveQueryIndex(index) {
|
||||
this.props.setActiveQueryIndex(index);
|
||||
},
|
||||
|
||||
handleAddQuery() {
|
||||
this.props.actions.addQuery();
|
||||
const newIndex = this.props.queries.length
|
||||
this.props.actions.addQuery()
|
||||
this.handleSetActiveQueryIndex(newIndex)
|
||||
},
|
||||
|
||||
handleAddRawQuery() {
|
||||
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`});
|
||||
},
|
||||
|
||||
handleDeleteQuery(query) {
|
||||
this.props.actions.deleteQuery(query.id);
|
||||
const newIndex = this.props.queries.length
|
||||
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`})
|
||||
this.handleSetActiveQueryIndex(newIndex)
|
||||
},
|
||||
|
||||
getActiveQuery() {
|
||||
const {queries, activeQueryID} = this.props;
|
||||
const activeQuery = queries.find((query) => query.id === activeQueryID);
|
||||
const defaultQuery = queries[0];
|
||||
const {queries, activeQueryIndex} = this.props
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
|
||||
return activeQuery || defaultQuery;
|
||||
return activeQuery || defaultQuery
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -80,7 +80,7 @@ const QueryBuilder = React.createClass({
|
|||
if (!query) {
|
||||
return (
|
||||
<div className="qeditor--empty">
|
||||
<h5>This Graph has no Queries</h5>
|
||||
<h5 className="no-user-select">This Graph has no Queries</h5>
|
||||
<br/>
|
||||
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
|
||||
</div>
|
||||
|
@ -90,7 +90,7 @@ const QueryBuilder = React.createClass({
|
|||
return (
|
||||
<QueryEditor
|
||||
timeRange={timeRange}
|
||||
query={this.getActiveQuery()}
|
||||
query={query}
|
||||
actions={actions}
|
||||
onAddQuery={this.handleAddQuery}
|
||||
/>
|
||||
|
@ -98,7 +98,7 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
renderQueryTabList() {
|
||||
const {queries} = this.props;
|
||||
const {queries, activeQueryIndex, onDeleteQuery} = this.props;
|
||||
return (
|
||||
<div className="query-builder--tabs">
|
||||
<div className="query-builder--tabs-heading">
|
||||
|
@ -114,15 +114,17 @@ const QueryBuilder = React.createClass({
|
|||
}
|
||||
return (
|
||||
<QueryTabItem
|
||||
isActive={this.getActiveQuery().id === q.id}
|
||||
key={q.id + i}
|
||||
isActive={i === activeQueryIndex}
|
||||
key={i}
|
||||
queryIndex={i}
|
||||
query={q}
|
||||
onSelect={this.handleSetActiveQuery}
|
||||
onDelete={this.handleDeleteQuery}
|
||||
onSelect={this.handleSetActiveQueryIndex}
|
||||
onDelete={onDeleteQuery}
|
||||
queryTabText={queryTabText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -147,14 +149,4 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(viewActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryBuilder);
|
||||
export default QueryBuilder
|
||||
|
|
|
@ -15,7 +15,7 @@ const {
|
|||
const QueryEditor = React.createClass({
|
||||
propTypes: {
|
||||
query: shape({
|
||||
id: string.isRequired,
|
||||
id: string,
|
||||
}).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
|
|
|
@ -10,15 +10,16 @@ const QueryTabItem = React.createClass({
|
|||
onSelect: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
queryTabText: PropTypes.string,
|
||||
queryIndex: PropTypes.number,
|
||||
},
|
||||
|
||||
handleSelect() {
|
||||
this.props.onSelect(this.props.query);
|
||||
this.props.onSelect(this.props.queryIndex)
|
||||
},
|
||||
|
||||
handleDelete(e) {
|
||||
e.stopPropagation();
|
||||
this.props.onDelete(this.props.query);
|
||||
this.props.onDelete(this.props.queryIndex)
|
||||
},
|
||||
|
||||
render() {
|
||||
|
|
|
@ -3,8 +3,11 @@ import selectStatement from '../utils/influxql/select';
|
|||
import classNames from 'classnames';
|
||||
import AutoRefresh from 'shared/components/AutoRefresh';
|
||||
import LineGraph from 'shared/components/LineGraph';
|
||||
import SingleStat from 'shared/components/SingleStat';
|
||||
import MultiTable from './MultiTable';
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph);
|
||||
const RefreshingSingleStat = AutoRefresh(SingleStat);
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
|
@ -15,6 +18,7 @@ const {
|
|||
|
||||
const Visualization = React.createClass({
|
||||
propTypes: {
|
||||
cellType: string,
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
|
@ -45,8 +49,32 @@ const Visualization = React.createClass({
|
|||
this.setState({isGraphInView: !this.state.isGraphInView});
|
||||
},
|
||||
|
||||
renderGraph(queries) {
|
||||
const {cellType, autoRefresh, activeQueryIndex} = this.props
|
||||
const isInDataExplorer = true;
|
||||
|
||||
if (cellType === 'single-stat') {
|
||||
return <RefreshingSingleStat queries={[queries[0]]} autoRefresh={autoRefresh} />
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: cellType === 'line-stepplot',
|
||||
stackedGraph: cellType === 'line-stacked',
|
||||
}
|
||||
return (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
autoRefresh={autoRefresh}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
showSingleStat={cellType === "line-plus-single-stat"}
|
||||
displayOptions={displayOptions}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {queryConfigs, autoRefresh, timeRange, activeQueryIndex, height, heightPixels} = this.props;
|
||||
const {queryConfigs, timeRange, height, heightPixels} = this.props;
|
||||
const {source} = this.context;
|
||||
const proxyLink = source.links.proxy;
|
||||
|
||||
|
@ -58,7 +86,6 @@ const Visualization = React.createClass({
|
|||
const queries = statements.filter((s) => s.text !== null).map((s) => {
|
||||
return {host: [proxyLink], text: s.text, id: s.id};
|
||||
});
|
||||
const isInDataExplorer = true;
|
||||
|
||||
return (
|
||||
<div className={classNames("graph", {active: true})} style={{height}}>
|
||||
|
@ -74,14 +101,9 @@ const Visualization = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
<div className={classNames({"graph-container": isGraphInView, "table-container": !isGraphInView})}>
|
||||
{isGraphInView ? (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
autoRefresh={autoRefresh}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
/>
|
||||
) : <MultiTable queries={queries} height={heightPixels} />}
|
||||
{isGraphInView ?
|
||||
this.renderGraph(queries) :
|
||||
<MultiTable queries={queries} height={heightPixels} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import QueryBuilder from '../components/QueryBuilder';
|
||||
import Visualization from '../components/Visualization';
|
||||
import Header from '../containers/Header';
|
||||
import ResizeContainer from 'src/shared/components/ResizeContainer';
|
||||
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
|
||||
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
import {setTimeRange as setTimeRangeAction} from '../actions/view';
|
||||
import * as viewActions from 'src/data_explorer/actions/view'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
|
@ -25,7 +28,8 @@ const DataExplorer = React.createClass({
|
|||
self: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}),
|
||||
queryConfigs: arrayOf(shape({})).isRequired,
|
||||
queryConfigActions: shape({}).isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
handleChooseAutoRefresh: func.isRequired,
|
||||
timeRange: shape({
|
||||
|
@ -53,18 +57,23 @@ const DataExplorer = React.createClass({
|
|||
|
||||
getInitialState() {
|
||||
return {
|
||||
activeQueryID: null,
|
||||
activeQueryIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
handleSetActiveQuery(id) {
|
||||
this.setState({activeQueryID: id});
|
||||
handleSetActiveQueryIndex(index) {
|
||||
this.setState({activeQueryIndex: index});
|
||||
},
|
||||
|
||||
handleDeleteQuery(index) {
|
||||
const {queryConfigs} = this.props
|
||||
const query = queryConfigs[index]
|
||||
this.props.queryConfigActions.deleteQuery(query.id)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props;
|
||||
const {activeQueryID} = this.state;
|
||||
const queries = dataExplorer.queryIDs.map((qid) => queryConfigs[qid]);
|
||||
const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, queryConfigActions} = this.props
|
||||
const {activeQueryIndex} = this.state
|
||||
|
||||
return (
|
||||
<div className="data-explorer">
|
||||
|
@ -77,17 +86,20 @@ const DataExplorer = React.createClass({
|
|||
<Visualization
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
queryConfigs={queries}
|
||||
activeQueryID={this.state.activeQueryID}
|
||||
queryConfigs={queryConfigs}
|
||||
activeQueryIndex={0}
|
||||
/>
|
||||
<QueryBuilder
|
||||
queries={queries}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
setActiveQuery={this.handleSetActiveQuery}
|
||||
activeQueryID={activeQueryID}
|
||||
/>
|
||||
<ResizeBottom>
|
||||
<QueryBuilder
|
||||
queries={queryConfigs}
|
||||
actions={queryConfigActions}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
setActiveQueryIndex={this.handleSetActiveQueryIndex}
|
||||
onDeleteQuery={this.handleDeleteQuery}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
/>
|
||||
</ResizeBottom>
|
||||
</ResizeContainer>
|
||||
</div>
|
||||
);
|
||||
|
@ -96,11 +108,12 @@ const DataExplorer = React.createClass({
|
|||
|
||||
function mapStateToProps(state) {
|
||||
const {app: {persisted: {autoRefresh}}, timeRange, queryConfigs, dataExplorer} = state;
|
||||
const queryConfigValues = _.values(queryConfigs)
|
||||
|
||||
return {
|
||||
autoRefresh,
|
||||
timeRange,
|
||||
queryConfigs,
|
||||
queryConfigs: queryConfigValues,
|
||||
dataExplorer,
|
||||
};
|
||||
}
|
||||
|
@ -108,7 +121,8 @@ function mapStateToProps(state) {
|
|||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
||||
setTimeRange: bindActionCreators(setTimeRangeAction, dispatch),
|
||||
setTimeRange: bindActionCreators(viewActions.setTimeRange, dispatch),
|
||||
queryConfigActions: bindActionCreators(viewActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import moment from 'moment';
|
||||
import {withRouter} from 'react-router';
|
||||
|
||||
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown';
|
||||
import SourceIndicator from '../../shared/components/SourceIndicator';
|
||||
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
|
||||
const {
|
||||
func,
|
||||
number,
|
||||
|
@ -38,16 +35,6 @@ const Header = React.createClass({
|
|||
this.props.actions.setTimeRange(bounds);
|
||||
},
|
||||
|
||||
findSelected({upper, lower}) {
|
||||
if (upper && lower) {
|
||||
const format = (t) => moment(t.replace(/\'/g, '')).format('YYYY-MM-DD HH:mm');
|
||||
return `${format(lower)} - ${format(upper)}`;
|
||||
}
|
||||
|
||||
const selected = timeRanges.find((range) => range.queryValue === lower);
|
||||
return selected ? selected.inputValue : 'Custom';
|
||||
},
|
||||
|
||||
render() {
|
||||
const {autoRefresh, actions: {handleChooseAutoRefresh}, timeRange} = this.props;
|
||||
|
||||
|
@ -60,7 +47,7 @@ const Header = React.createClass({
|
|||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={this.context.source.name} />
|
||||
<AutoRefreshDropdown onChoose={handleChooseAutoRefresh} selected={autoRefresh} iconName="refresh" />
|
||||
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={this.findSelected(timeRange)} />
|
||||
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={timeRange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,12 +20,12 @@ export default function queryConfigs(state = {}, action) {
|
|||
}
|
||||
|
||||
case 'CHOOSE_NAMESPACE': {
|
||||
const {queryId, database, retentionPolicy} = action.payload;
|
||||
const nextQueryConfig = chooseNamespace(state[queryId], {database, retentionPolicy});
|
||||
const {queryId, database, retentionPolicy} = action.payload
|
||||
const nextQueryConfig = chooseNamespace(state[queryId], {database, retentionPolicy})
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: Object.assign(nextQueryConfig, {rawText: state[queryId].rawText}),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
case 'CHOOSE_MEASUREMENT': {
|
||||
|
@ -84,20 +84,20 @@ export default function queryConfigs(state = {}, action) {
|
|||
}
|
||||
|
||||
case 'TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryId} = action.payload;
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryId]);
|
||||
const {queryId} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryId])
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
case 'DELETE_QUERY': {
|
||||
const {queryID} = action.payload;
|
||||
const nextState = update(state, {$apply: (configs) => {
|
||||
delete configs[queryID];
|
||||
delete configs[queryID]
|
||||
return configs;
|
||||
}});
|
||||
}})
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
|
||||
|
||||
const initialLower = timeRanges[1].lower
|
||||
const initialUpper = timeRanges[1].upper
|
||||
|
||||
const initialState = {
|
||||
upper: null,
|
||||
lower: 'now() - 15m',
|
||||
};
|
||||
upper: initialUpper,
|
||||
lower: initialLower,
|
||||
}
|
||||
|
||||
export default function timeRange(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
|
@ -15,5 +20,5 @@ export default function timeRange(state = initialState, action) {
|
|||
return {...state, ...newState};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -1,21 +1,33 @@
|
|||
export default function selectStatement(timeBounds, config) {
|
||||
const {database, measurement, fields, groupBy, tags, retentionPolicy, areTagsAccepted} = config;
|
||||
const {groupBy, tags, areTagsAccepted} = config;
|
||||
const {upper, lower} = timeBounds;
|
||||
|
||||
if (!database || !measurement || !fields || !fields.length) {
|
||||
return null;
|
||||
const select = _buildSelect(config)
|
||||
if (select === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : '';
|
||||
const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`;
|
||||
|
||||
const fieldsClause = _buildFields(fields);
|
||||
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted});
|
||||
const dimensions = _buildGroupBy(groupBy);
|
||||
|
||||
return `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}${condition}${dimensions}`;
|
||||
return `${select}${condition}${dimensions}`;
|
||||
}
|
||||
|
||||
function _buildSelect({fields, database, retentionPolicy, measurement}) {
|
||||
if (!database || !measurement || !fields || !fields.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : ''
|
||||
const fieldsClause = _buildFields(fields)
|
||||
const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`
|
||||
const statement = `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}`
|
||||
return statement
|
||||
}
|
||||
|
||||
export function buildSelectStatement(config) {
|
||||
return _buildSelect(config)
|
||||
}
|
||||
|
||||
function _buildFields(fieldFuncs) {
|
||||
const hasAggregate = fieldFuncs.some((f) => f.funcs && f.funcs.length);
|
||||
|
@ -86,4 +98,3 @@ function _buildGroupByTags(groupBy) {
|
|||
|
||||
return ` GROUP BY ${tags}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ export const HostPage = React.createClass({
|
|||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find((range) => range.queryValue === lower);
|
||||
const timeRange = timeRanges.find((range) => range.lower === lower);
|
||||
this.setState({timeRange});
|
||||
},
|
||||
|
||||
|
|
|
@ -97,8 +97,8 @@ export const DataSection = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {query, timeRange} = this.props;
|
||||
const statement = query.rawText || selectStatement({lower: timeRange.queryValue}, query) || `SELECT "fields" FROM "db"."rp"."measurement"`;
|
||||
const {query, timeRange: {lower}} = this.props;
|
||||
const statement = query.rawText || selectStatement({lower}, query) || `SELECT "fields" FROM "db"."rp"."measurement"`;
|
||||
|
||||
return (
|
||||
<div className="kapacitor-rule-section">
|
||||
|
|
|
@ -73,7 +73,7 @@ export const KapacitorRule = React.createClass({
|
|||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find((range) => range.queryValue === lower);
|
||||
const timeRange = timeRanges.find((range) => range.lower === lower);
|
||||
this.setState({timeRange});
|
||||
},
|
||||
|
||||
|
|
|
@ -25,9 +25,9 @@ export const RuleGraph = React.createClass({
|
|||
},
|
||||
|
||||
renderGraph() {
|
||||
const {query, source, timeRange, rule} = this.props;
|
||||
const {query, source, timeRange: {lower}, rule} = this.props;
|
||||
const autoRefreshMs = 30000;
|
||||
const queryText = selectStatement({lower: timeRange.queryValue}, query);
|
||||
const queryText = selectStatement({lower}, query);
|
||||
const queries = [{host: source.links.proxy, text: queryText}];
|
||||
const kapacitorLineColors = ["#4ED8A0"];
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ export const RuleHeader = React.createClass({
|
|||
return (
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
<TimeRangeDropdown onChooseTimeRange={onChooseTimeRange} selected={timeRange.inputValue} />
|
||||
<TimeRangeDropdown onChooseTimeRange={onChooseTimeRange} selected={timeRange} />
|
||||
{saveButton}
|
||||
<ReactTooltip id="save-kapacitor-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip kapacitor-tooltip place-bottom" />
|
||||
</div>
|
||||
|
|
|
@ -65,7 +65,7 @@ export const KubernetesDashboard = React.createClass({
|
|||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find((range) => range.queryValue === lower);
|
||||
const timeRange = timeRanges.find((range) => range.lower === lower);
|
||||
this.setState({timeRange});
|
||||
},
|
||||
|
||||
|
|
|
@ -165,6 +165,8 @@ export default React.createClass({
|
|||
valueRange: getRange(timeSeries, ranges.y2),
|
||||
},
|
||||
},
|
||||
stepPlot: options.stepPlot,
|
||||
stackedGraph: options.stackedGraph,
|
||||
underlayCallback: options.underlayCallback,
|
||||
series: dygraphSeries,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,9 @@ import LineGraph from 'shared/components/LineGraph';
|
|||
import SingleStat from 'shared/components/SingleStat';
|
||||
import NameableGraph from 'shared/components/NameableGraph';
|
||||
import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
|
||||
|
||||
import timeRanges from 'hson!../data/timeRanges.hson';
|
||||
|
||||
const GridLayout = WidthProvider(ReactGridLayout);
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph);
|
||||
|
@ -21,23 +24,15 @@ export const LayoutRenderer = React.createClass({
|
|||
propTypes: {
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({
|
||||
defaultGroupBy: string.isRequired,
|
||||
queryValue: string.isRequired,
|
||||
lower: string.isRequired,
|
||||
}).isRequired,
|
||||
cells: arrayOf(
|
||||
shape({
|
||||
queries: arrayOf(
|
||||
shape({
|
||||
label: string,
|
||||
range: shape({
|
||||
upper: number,
|
||||
lower: number,
|
||||
}),
|
||||
rp: string,
|
||||
text: string.isRequired,
|
||||
database: string.isRequired,
|
||||
groupbys: arrayOf(string),
|
||||
wheres: arrayOf(string),
|
||||
text: string,
|
||||
query: string,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
x: number.isRequired,
|
||||
|
@ -46,6 +41,7 @@ export const LayoutRenderer = React.createClass({
|
|||
h: number.isRequired,
|
||||
i: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
host: string,
|
||||
|
@ -54,15 +50,17 @@ export const LayoutRenderer = React.createClass({
|
|||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
onSummonOverlayTechnologies: func,
|
||||
},
|
||||
|
||||
buildQuery(q) {
|
||||
const {timeRange, host} = this.props;
|
||||
const {wheres, groupbys} = q;
|
||||
const {timeRange: {lower}, host} = this.props
|
||||
const {defaultGroupBy} = timeRanges.find((range) => range.lower === lower)
|
||||
const {wheres, groupbys} = q
|
||||
|
||||
let text = q.text;
|
||||
|
||||
text += ` where time > ${timeRange.queryValue}`;
|
||||
text += ` where time > ${lower}`;
|
||||
|
||||
if (host) {
|
||||
text += ` and \"host\" = '${host}'`;
|
||||
|
@ -76,25 +74,25 @@ export const LayoutRenderer = React.createClass({
|
|||
if (groupbys.find((g) => g.includes("time"))) {
|
||||
text += ` group by ${groupbys.join(',')}`;
|
||||
} else if (groupbys.length > 0) {
|
||||
text += ` group by time(${timeRange.defaultGroupBy}),${groupbys.join(',')}`;
|
||||
text += ` group by time(${defaultGroupBy}),${groupbys.join(',')}`;
|
||||
} else {
|
||||
text += ` group by time(${timeRange.defaultGroupBy})`;
|
||||
text += ` group by time(${defaultGroupBy})`;
|
||||
}
|
||||
} else {
|
||||
text += ` group by time(${timeRange.defaultGroupBy})`;
|
||||
text += ` group by time(${defaultGroupBy})`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
|
||||
generateVisualizations() {
|
||||
const {autoRefresh, source, cells, onEditCell, onRenameCell, onUpdateCell} = this.props;
|
||||
const {autoRefresh, source, cells, onEditCell, onRenameCell, onUpdateCell, onSummonOverlayTechnologies} = this.props;
|
||||
|
||||
return cells.map((cell) => {
|
||||
const qs = cell.queries.map((q) => {
|
||||
return Object.assign({}, q, {
|
||||
const qs = cell.queries.map((query) => {
|
||||
return Object.assign({}, query, {
|
||||
host: source,
|
||||
text: this.buildQuery(q),
|
||||
text: this.buildQuery(query),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,6 +103,7 @@ export const LayoutRenderer = React.createClass({
|
|||
onEditCell={onEditCell}
|
||||
onRenameCell={onRenameCell}
|
||||
onUpdateCell={onUpdateCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
cell={cell}
|
||||
>
|
||||
<RefreshingSingleStat queries={[qs[0]]} autoRefresh={autoRefresh} />
|
||||
|
@ -124,12 +123,13 @@ export const LayoutRenderer = React.createClass({
|
|||
onEditCell={onEditCell}
|
||||
onRenameCell={onRenameCell}
|
||||
onUpdateCell={onUpdateCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
cell={cell}
|
||||
>
|
||||
<RefreshingLineGraph
|
||||
queries={qs}
|
||||
autoRefresh={autoRefresh}
|
||||
showSingleStat={cell.type === "line-plus-single-stat"}
|
||||
showSingleStat={cell.type === 'line-plus-single-stat'}
|
||||
displayOptions={displayOptions}
|
||||
/>
|
||||
</NameableGraph>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import Dygraph from './Dygraph';
|
||||
import Dygraph from 'shared/components/Dygraph';
|
||||
import classNames from 'classnames';
|
||||
import shallowCompare from 'react-addons-shallow-compare';
|
||||
import _ from 'lodash';
|
||||
|
|
|
@ -1,60 +1,9 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const NameableGraph = ({
|
||||
cell: {
|
||||
name,
|
||||
isEditing,
|
||||
x,
|
||||
y,
|
||||
},
|
||||
cell,
|
||||
children,
|
||||
onEditCell,
|
||||
onRenameCell,
|
||||
onUpdateCell,
|
||||
}) => {
|
||||
let nameOrField
|
||||
const isEditable = !!(onEditCell || onRenameCell || onUpdateCell)
|
||||
|
||||
if (isEditing && isEditable) {
|
||||
nameOrField = (
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
autoFocus={true}
|
||||
onChange={onRenameCell(x, y)}
|
||||
onBlur={onUpdateCell(cell)}
|
||||
onKeyUp={(evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
onUpdateCell(cell)()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
nameOrField = name
|
||||
}
|
||||
|
||||
let onClickHandler
|
||||
if (isEditable) {
|
||||
onClickHandler = onEditCell
|
||||
} else {
|
||||
onClickHandler = () => {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="dash-graph--heading" onClick={onClickHandler(x, y, isEditing)}>{nameOrField}</h2>
|
||||
<div className="dash-graph--container">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'react-onclickoutside'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
node,
|
||||
number,
|
||||
|
@ -62,16 +11,109 @@ const {
|
|||
string,
|
||||
} = PropTypes
|
||||
|
||||
NameableGraph.propTypes = {
|
||||
cell: shape({
|
||||
name: string.isRequired,
|
||||
x: number.isRequired,
|
||||
y: number.isRequired,
|
||||
}).isRequired,
|
||||
children: node.isRequired,
|
||||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
}
|
||||
const NameableGraph = React.createClass({
|
||||
propTypes: {
|
||||
cell: shape({
|
||||
name: string.isRequired,
|
||||
isEditing: bool,
|
||||
x: number.isRequired,
|
||||
y: number.isRequired,
|
||||
}).isRequired,
|
||||
children: node.isRequired,
|
||||
onEditCell: func,
|
||||
onRenameCell: func,
|
||||
onUpdateCell: func,
|
||||
onSummonOverlayTechnologies: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
isMenuOpen: false,
|
||||
}
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
this.setState({
|
||||
isMenuOpen: !this.state.isMenuOpen,
|
||||
})
|
||||
},
|
||||
|
||||
closeMenu() {
|
||||
this.setState({
|
||||
isMenuOpen: false,
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
cell,
|
||||
cell: {
|
||||
x,
|
||||
y,
|
||||
name,
|
||||
isEditing,
|
||||
},
|
||||
onEditCell,
|
||||
onRenameCell,
|
||||
onUpdateCell,
|
||||
onSummonOverlayTechnologies,
|
||||
children,
|
||||
} = this.props
|
||||
|
||||
const isEditable = !!(onEditCell || onRenameCell || onUpdateCell)
|
||||
|
||||
let nameOrField
|
||||
if (isEditing && isEditable) {
|
||||
nameOrField = (
|
||||
<input
|
||||
className="form-control input-sm dash-graph--name-edit"
|
||||
type="text"
|
||||
value={name}
|
||||
autoFocus={true}
|
||||
onChange={onRenameCell(x, y)}
|
||||
onBlur={onUpdateCell(cell)}
|
||||
onKeyUp={(evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
onUpdateCell(cell)()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
nameOrField = (<span className="dash-graph--name">{name}</span>)
|
||||
}
|
||||
|
||||
let onClickHandler
|
||||
if (isEditable) {
|
||||
onClickHandler = onEditCell
|
||||
} else {
|
||||
onClickHandler = () => {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dash-graph">
|
||||
<div className="dash-graph--heading">
|
||||
<div onClick={onClickHandler(x, y, isEditing)}>{nameOrField}</div>
|
||||
<ContextMenu isOpen={this.state.isMenuOpen} toggleMenu={this.toggleMenu} onSummonOverlayTechnologies={onSummonOverlayTechnologies} cell={cell} handleClickOutside={this.closeMenu}/>
|
||||
</div>
|
||||
<div className="dash-graph--container">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const ContextMenu = OnClickOutside(({isOpen, toggleMenu, onSummonOverlayTechnologies, cell}) => (
|
||||
<div className={classnames("dash-graph--options", {"dash-graph--options-show": isOpen})} onClick={toggleMenu}>
|
||||
<button className="btn btn-info btn-xs">
|
||||
<span className="icon caret-down"></span>
|
||||
</button>
|
||||
<ul className="dash-graph--options-menu">
|
||||
<li onClick={() => onSummonOverlayTechnologies(cell)}>Edit</li>
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
export default NameableGraph;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import ResizeHandle from 'shared/components/ResizeHandle';
|
||||
|
||||
const {node} = PropTypes;
|
||||
const {
|
||||
node,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
||||
const ResizeContainer = React.createClass({
|
||||
propTypes: {
|
||||
children: node.isRequired,
|
||||
|
@ -47,7 +51,7 @@ const ResizeContainer = React.createClass({
|
|||
|
||||
// Don't trigger a resize if the new sizes are too small
|
||||
const minTopPanelHeight = 200;
|
||||
const minBottomPanelHeight = 100;
|
||||
const minBottomPanelHeight = 200;
|
||||
const topHeightPixels = ((newTopPanelPercent / turnToPercent) * appHeight);
|
||||
const bottomHeightPixels = ((newBottomPanelPercent / turnToPercent) * appHeight);
|
||||
|
||||
|
@ -58,20 +62,38 @@ const ResizeContainer = React.createClass({
|
|||
this.setState({topHeight: `${(newTopPanelPercent)}%`, bottomHeight: `${(newBottomPanelPercent)}%`, topHeightPixels});
|
||||
},
|
||||
|
||||
render() {
|
||||
const {topHeight, bottomHeight, isDragging, topHeightPixels} = this.state;
|
||||
const top = React.cloneElement(this.props.children[0], {height: topHeight, heightPixels: topHeightPixels});
|
||||
const bottom = React.cloneElement(this.props.children[1], {height: bottomHeight, top: topHeight});
|
||||
const handle = <ResizeHandle isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} top={topHeight} />;
|
||||
renderHandle() {
|
||||
const {isDragging, topHeight} = this.state
|
||||
return (
|
||||
<ResizeHandle isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} top={topHeight} />
|
||||
)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {topHeight, topHeightPixels, bottomHeight} = this.state
|
||||
const top = React.cloneElement(this.props.children[0], {height: topHeight, heightPixels: topHeightPixels})
|
||||
const bottom = React.cloneElement(this.props.children[1], {height: bottomHeight})
|
||||
return (
|
||||
<div className="resize-container page-contents" onMouseLeave={this.handleMouseLeave} onMouseUp={this.handleStopDrag} onMouseMove={this.handleDrag} ref="resizeContainer" >
|
||||
{top}
|
||||
{handle}
|
||||
{this.renderHandle()}
|
||||
{bottom}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default ResizeContainer;
|
||||
const ResizeBottom = (props) => (
|
||||
<div className="resize-bottom" style={{height: props.height}}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
ResizeBottom.propTypes = {
|
||||
children: node.isRequired,
|
||||
height: string,
|
||||
}
|
||||
|
||||
export {ResizeBottom}
|
||||
|
||||
export default ResizeContainer
|
||||
|
|
|
@ -2,13 +2,15 @@ import React from 'react';
|
|||
import classnames from 'classnames';
|
||||
import OnClickOutside from 'shared/components/OnClickOutside';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import timeRanges from 'hson!../data/timeRanges.hson';
|
||||
|
||||
const TimeRangeDropdown = React.createClass({
|
||||
autobind: false,
|
||||
|
||||
propTypes: {
|
||||
selected: React.PropTypes.string.isRequired,
|
||||
selected: React.PropTypes.shape().isRequired,
|
||||
onChooseTimeRange: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
|
@ -18,16 +20,26 @@ const TimeRangeDropdown = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
findTimeRangeInputValue({upper, lower}) {
|
||||
if (upper && lower) {
|
||||
const format = (t) => moment(t.replace(/\'/g, '')).format('YYYY-MM-DD HH:mm');
|
||||
return `${format(lower)} - ${format(upper)}`;
|
||||
}
|
||||
|
||||
const selected = timeRanges.find((range) => range.lower === lower);
|
||||
return selected ? selected.inputValue : 'Custom';
|
||||
},
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isOpen: false});
|
||||
},
|
||||
|
||||
handleSelection(params) {
|
||||
const {queryValue, menuOption} = params;
|
||||
const {lower, upper, menuOption} = params;
|
||||
if (menuOption.toLowerCase() === 'custom') {
|
||||
this.props.onChooseTimeRange({custom: true});
|
||||
} else {
|
||||
this.props.onChooseTimeRange({lower: queryValue, upper: null});
|
||||
this.props.onChooseTimeRange({lower, upper});
|
||||
}
|
||||
this.setState({isOpen: false});
|
||||
},
|
||||
|
@ -45,7 +57,7 @@ const TimeRangeDropdown = React.createClass({
|
|||
<div className="dropdown time-range-dropdown">
|
||||
<div className="btn btn-sm btn-info dropdown-toggle" onClick={() => self.toggleMenu()}>
|
||||
<span className="icon clock"></span>
|
||||
<span className="selected-time-range">{selected}</span>
|
||||
<span className="selected-time-range">{self.findTimeRangeInputValue(selected)}</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
<ul className={classnames("dropdown-menu", {show: isOpen})}>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{type: "line", menuOption: "Line"},
|
||||
{type: "line-stacked", menuOption: "Stacked"},
|
||||
{type: "line-stepplot", menuOption: "Step-Plot"},
|
||||
{type: "single-stat", menuOption: "SingleStat"},
|
||||
{type: "line-plus-single-stat", menuOption: "Line + Stat"},
|
||||
]
|
|
@ -1,11 +1,11 @@
|
|||
[
|
||||
{defaultGroupBy: '10s', seconds: 300, inputValue: 'Past 5 minutes', queryValue: 'now() - 5m', menuOption: 'Past 5 minutes'},
|
||||
{defaultGroupBy: '1m', seconds: 900, inputValue: 'Past 15 minutes', queryValue: 'now() - 15m', menuOption: 'Past 15 minutes'},
|
||||
{defaultGroupBy: '1m', seconds: 3600, inputValue: 'Past hour', queryValue: 'now() - 1h', menuOption: 'Past hour'},
|
||||
{defaultGroupBy: '1m', seconds: 21600, inputValue: 'Past 6 hours', queryValue: 'now() - 6h', menuOption: 'Past 6 hours'},
|
||||
{defaultGroupBy: '5m', seconds: 43200, inputValue: 'Past 12 hours', queryValue: 'now() - 12h', menuOption: 'Past 12 hours'},
|
||||
{defaultGroupBy: '10m', seconds: 86400, inputValue: 'Past 24 hours', queryValue: 'now() - 24h', menuOption: 'Past 24 hours'},
|
||||
{defaultGroupBy: '30m', seconds: 172800, inputValue: 'Past 2 days', queryValue: 'now() - 2d', menuOption: 'Past 2 days'},
|
||||
{defaultGroupBy: '1h', seconds: 604800, inputValue: 'Past 7 days', queryValue: 'now() - 7d', menuOption: 'Past 7 days'},
|
||||
{defaultGroupBy: '6h', seconds: 2592000, inputValue: 'Past 30 days', queryValue: 'now() - 30d', menuOption: 'Past 30 days'},
|
||||
{defaultGroupBy: '10s', seconds: 300, inputValue: 'Past 5 minutes', lower: 'now() - 5m', upper: null, menuOption: 'Past 5 minutes'},
|
||||
{defaultGroupBy: '1m', seconds: 900, inputValue: 'Past 15 minutes', lower: 'now() - 15m', upper: null, menuOption: 'Past 15 minutes'},
|
||||
{defaultGroupBy: '1m', seconds: 3600, inputValue: 'Past hour', lower: 'now() - 1h', upper: null, menuOption: 'Past hour'},
|
||||
{defaultGroupBy: '1m', seconds: 21600, inputValue: 'Past 6 hours', lower: 'now() - 6h', upper: null, menuOption: 'Past 6 hours'},
|
||||
{defaultGroupBy: '5m', seconds: 43200, inputValue: 'Past 12 hours', lower: 'now() - 12h', upper: null, menuOption: 'Past 12 hours'},
|
||||
{defaultGroupBy: '10m', seconds: 86400, inputValue: 'Past 24 hours', lower: 'now() - 24h', upper: null, menuOption: 'Past 24 hours'},
|
||||
{defaultGroupBy: '30m', seconds: 172800, inputValue: 'Past 2 days', lower: 'now() - 2d', upper: null, menuOption: 'Past 2 days'},
|
||||
{defaultGroupBy: '1h', seconds: 604800, inputValue: 'Past 7 days', lower: 'now() - 7d', upper: null, menuOption: 'Past 7 days'},
|
||||
{defaultGroupBy: '6h', seconds: 2592000, inputValue: 'Past 30 days', lower: 'now() - 30d', upper: null, menuOption: 'Past 30 days'},
|
||||
]
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
@import 'components/tables';
|
||||
@import 'components/resizer';
|
||||
@import 'components/source-indicator';
|
||||
@import 'components/confirm-buttons';
|
||||
|
||||
// Pages
|
||||
@import 'pages/alerts';
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Confirmation Buttons
|
||||
------------------------------------------------------
|
||||
*/
|
||||
.confirm-buttons {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
& > * {
|
||||
margin: 0 0 0 4px !important;
|
||||
|
||||
&:first-child {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
span.icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
|
@ -86,3 +86,22 @@ $resizer-color-active: $c-pool;
|
|||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.resize-container {
|
||||
.resize-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 60%;
|
||||
width: 100%;
|
||||
}
|
||||
.resize-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
height: 40%;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -48,7 +48,15 @@ $dash-graph-heading: 30px;
|
|||
left: 0;
|
||||
}
|
||||
}
|
||||
.dash-graph {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.dash-graph--container {
|
||||
z-index: 1;
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
|
@ -83,6 +91,7 @@ $dash-graph-heading: 30px;
|
|||
}
|
||||
}
|
||||
.dash-graph--heading {
|
||||
z-index: 2;
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
|
@ -97,6 +106,7 @@ $dash-graph-heading: 30px;
|
|||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: $radius;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
|
@ -108,11 +118,158 @@ $dash-graph-heading: 30px;
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
.dash-graph--name-edit,
|
||||
.dash-graph--name {
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
height: 26px !important;
|
||||
line-height: (26px - 4px) !important;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
left: -6px;
|
||||
border-radius: $radius;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dash-graph--name {
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
border: 2px solid $g3-castle;
|
||||
top: 2px;
|
||||
|
||||
&:after {
|
||||
display: inline-block;
|
||||
content: "\f058";
|
||||
font-family: 'icomoon' !important;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
color: $g11-sidewalk;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
color: $g20-white;
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dash-graph--name-edit {
|
||||
top: -1px;
|
||||
width: 190px;
|
||||
}
|
||||
.dash-graph--options {
|
||||
height: $dash-graph-heading;
|
||||
position: relative;
|
||||
width: ($dash-graph-heading - 8px);
|
||||
right: -6px;
|
||||
|
||||
> .btn {
|
||||
background-color: transparent !important;
|
||||
padding: 0;
|
||||
margin: 4px 0;
|
||||
height: ($dash-graph-heading - 8px);
|
||||
width: ($dash-graph-heading - 8px);
|
||||
line-height: ($dash-graph-heading - 8px);
|
||||
transition:
|
||||
background-color 0.25s ease,
|
||||
color 0.25s ease !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $g5-pepper !important;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
$dash-graph-options-arrow: 8px;
|
||||
|
||||
.presentation-mode .dash-graph--options {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
.dash-graph--options-menu {
|
||||
position: absolute;
|
||||
top: ($dash-graph-heading + $dash-graph-options-arrow);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: block;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
width: 90px;
|
||||
visibility: hidden;
|
||||
transition-property: all;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
background-color: $g5-pepper;
|
||||
padding: 0 11px;
|
||||
color: $g15-platinum;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $radius $radius 0 0;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: $dash-graph-options-arrow;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent $g5-pepper transparent;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -($dash-graph-options-arrow * 2);
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 0 $radius $radius;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu Open State */
|
||||
.dash-graph--options.dash-graph--options-show {
|
||||
> .btn {
|
||||
color: $g20-white;
|
||||
background-color: $g5-pepper !important;
|
||||
}
|
||||
.dash-graph--options-menu { visibility: visible; }
|
||||
.dash-graph--options-menu > li { opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
.graph-panel__refreshing {
|
||||
position: absolute;
|
||||
top: -18px !important;
|
||||
transform: translate(0,0);
|
||||
right: 16px !important;
|
||||
right: 50px !important;
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
|
||||
|
@ -227,3 +384,91 @@ $dash-graph-heading: 30px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Cell Edit Mode
|
||||
------------------------------------------------------
|
||||
*/
|
||||
$overlay-controls-height: 50px;
|
||||
$overlay-controls-bg: $g2-kevlar;
|
||||
$overlay-bg: rgba($g0-obsidian, 0.7);
|
||||
|
||||
.overlay-technology {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: $overlay-bg;
|
||||
|
||||
.overlay-controls {
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 $overlay-controls-height;
|
||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||
left: $explorer-page-padding;
|
||||
margin-top: $de-vertical-margin;
|
||||
border: 0;
|
||||
border-radius: $radius;
|
||||
@include gradient-h($g3-castle,$overlay-controls-bg);
|
||||
|
||||
/* Hack for making the adjacent query builder have less margin on top */
|
||||
& + .query-builder .query-builder--tabs,
|
||||
& + .query-builder .query-builder--tab-contents,
|
||||
& + .query-builder .qeditor--empty {
|
||||
margin-top: 2px;
|
||||
height: calc(100% - 18px);
|
||||
}
|
||||
}
|
||||
.overlay-controls--right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.toggle {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
p {
|
||||
font-weight: 600;
|
||||
color: $g13-mist;
|
||||
margin: 0;
|
||||
@include no-user-select;
|
||||
}
|
||||
}
|
||||
.overlay--graph-name {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
@include no-user-select;
|
||||
}
|
||||
.confirm-buttons {
|
||||
margin-left: 32px;
|
||||
}
|
||||
.confirm-buttons .btn {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 9px;
|
||||
|
||||
& > span.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.overlay-controls .toggle {
|
||||
|
||||
.toggle-btn {
|
||||
background-color: $overlay-controls-bg;
|
||||
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
&.active {
|
||||
background-color: $g5-pepper;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
.query-builder {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
flex: 1 0 0;
|
||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||
left: $explorer-page-padding;
|
||||
height: 40%;
|
||||
top: 60%;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
@ -206,6 +205,8 @@ $query-builder--column-heading-height: 60px;
|
|||
top: $query-builder--column-heading-height;
|
||||
left: 0;
|
||||
background-color: transparent;
|
||||
|
||||
p,h1,h2,h3,h4,h5,h6 { @include no-user-select(); }
|
||||
}
|
||||
}
|
||||
.query-builder--column:nth-of-type(1) { left: 0; }
|
||||
|
@ -226,4 +227,4 @@ $query-builder--column-heading-height: 60px;
|
|||
border-color: $g5-pepper;
|
||||
border-color: $g6-smoke;
|
||||
color: $g12-forge;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
.graph {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||
left: $explorer-page-padding;
|
||||
top: 0;
|
||||
height: 60%;
|
||||
}
|
||||
.graph-heading {
|
||||
position: relative;
|
||||
|
|
Loading…
Reference in New Issue