Merge pull request #1304 from influxdata/feature/template-variables
Feature/template variablespull/10616/head
commit
76195861fc
|
@ -26,9 +26,13 @@
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
1. [#1292](https://github.com/influxdata/chronograf/pull/1292): Introduce Template Variable Manager
|
||||||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||||
1. [#1265](https://github.com/influxdata/chronograf/pull/1265): Refactor the router to use auth and force /login route when auth expires
|
1. [#1265](https://github.com/influxdata/chronograf/pull/1265): Refactor the router to use auth and force /login route when auth expires
|
||||||
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
||||||
|
1. [#1316](https://github.com/influxdata/chronograf/pull/1316): Add templates API scoped within a dashboard
|
||||||
|
1. [#1311](https://github.com/influxdata/chronograf/pull/1311): Display currently selected values in TVControlBar
|
||||||
|
1. [#1315](https://github.com/influxdata/chronograf/pull/1315): Send selected TV values to proxy
|
||||||
1. [#1302](https://github.com/influxdata/chronograf/pull/1302): Add support for multiple Kapacitors per InfluxDB source
|
1. [#1302](https://github.com/influxdata/chronograf/pull/1302): Add support for multiple Kapacitors per InfluxDB source
|
||||||
|
|
||||||
### UI Improvements
|
### UI Improvements
|
||||||
|
|
|
@ -676,7 +676,7 @@
|
||||||
* node-libs-browser 0.6.0 [MIT](https://github.com/webpack/node-libs-browser)
|
* node-libs-browser 0.6.0 [MIT](https://github.com/webpack/node-libs-browser)
|
||||||
* node-notifier 4.6.1 [MIT](ssh://git@github.com/mikaelbr/node-notifier)
|
* node-notifier 4.6.1 [MIT](ssh://git@github.com/mikaelbr/node-notifier)
|
||||||
* node-pre-gyp 0.6.29 [BSD-3-Clause](http://github.com/mapbox/node-pre-gyp)
|
* node-pre-gyp 0.6.29 [BSD-3-Clause](http://github.com/mapbox/node-pre-gyp)
|
||||||
* node-sass 3.11.3 [MIT](https://github.com/sass/node-sass)
|
* node-sass 4.5.2 [MIT](https://github.com/sass/node-sass)
|
||||||
* node-uuid 1.4.7 [MIT](https://github.com/broofa/node-uuid)
|
* node-uuid 1.4.7 [MIT](https://github.com/broofa/node-uuid)
|
||||||
* nopt 3.0.6 [ISC](https://github.com/npm/nopt)
|
* nopt 3.0.6 [ISC](https://github.com/npm/nopt)
|
||||||
* normalize-package-data 2.3.5 [BSD;BSD-2-Clause](http://github.com/npm/normalize-package-data)
|
* normalize-package-data 2.3.5 [BSD;BSD-2-Clause](http://github.com/npm/normalize-package-data)
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -29,7 +29,7 @@ define CHRONOGIRAFFE
|
||||||
,"" _\_
|
,"" _\_
|
||||||
," ## | 0 0.
|
," ## | 0 0.
|
||||||
," ## ,-\__ `.
|
," ## ,-\__ `.
|
||||||
," / `--._;) - "HAI, I'm Chronogiraffe. Will you be my friend?"
|
," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!"
|
||||||
," ## /
|
," ## /
|
||||||
," ## /
|
," ## /
|
||||||
endef
|
endef
|
||||||
|
|
|
@ -190,11 +190,41 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
||||||
Type: c.Type,
|
Type: c.Type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
templates := make([]*Template, len(d.Templates))
|
||||||
|
for i, t := range d.Templates {
|
||||||
|
vals := make([]*TemplateValue, len(t.Values))
|
||||||
|
for j, v := range t.Values {
|
||||||
|
vals[j] = &TemplateValue{
|
||||||
|
Selected: v.Selected,
|
||||||
|
Type: v.Type,
|
||||||
|
Value: v.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &Template{
|
||||||
|
ID: string(t.ID),
|
||||||
|
TempVar: t.Var,
|
||||||
|
Values: vals,
|
||||||
|
Type: t.Type,
|
||||||
|
Label: t.Label,
|
||||||
|
}
|
||||||
|
if t.Query != nil {
|
||||||
|
template.Query = &TemplateQuery{
|
||||||
|
Command: t.Query.Command,
|
||||||
|
Db: t.Query.DB,
|
||||||
|
Rp: t.Query.RP,
|
||||||
|
Measurement: t.Query.Measurement,
|
||||||
|
TagKey: t.Query.TagKey,
|
||||||
|
FieldKey: t.Query.FieldKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templates[i] = template
|
||||||
|
}
|
||||||
return proto.Marshal(&Dashboard{
|
return proto.Marshal(&Dashboard{
|
||||||
ID: int64(d.ID),
|
ID: int64(d.ID),
|
||||||
Cells: cells,
|
Cells: cells,
|
||||||
Name: d.Name,
|
Templates: templates,
|
||||||
|
Name: d.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,8 +262,44 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
||||||
Type: c.Type,
|
Type: c.Type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templates := make([]chronograf.Template, len(pb.Templates))
|
||||||
|
for i, t := range pb.Templates {
|
||||||
|
vals := make([]chronograf.TemplateValue, len(t.Values))
|
||||||
|
for j, v := range t.Values {
|
||||||
|
vals[j] = chronograf.TemplateValue{
|
||||||
|
Selected: v.Selected,
|
||||||
|
Type: v.Type,
|
||||||
|
Value: v.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template := chronograf.Template{
|
||||||
|
ID: chronograf.TemplateID(t.ID),
|
||||||
|
TemplateVar: chronograf.TemplateVar{
|
||||||
|
Var: t.TempVar,
|
||||||
|
Values: vals,
|
||||||
|
},
|
||||||
|
Type: t.Type,
|
||||||
|
Label: t.Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Query != nil {
|
||||||
|
template.Query = &chronograf.TemplateQuery{
|
||||||
|
Command: t.Query.Command,
|
||||||
|
DB: t.Query.Db,
|
||||||
|
RP: t.Query.Rp,
|
||||||
|
Measurement: t.Query.Measurement,
|
||||||
|
TagKey: t.Query.TagKey,
|
||||||
|
FieldKey: t.Query.FieldKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templates[i] = template
|
||||||
|
}
|
||||||
|
|
||||||
d.ID = chronograf.DashboardID(pb.ID)
|
d.ID = chronograf.DashboardID(pb.ID)
|
||||||
d.Cells = cells
|
d.Cells = cells
|
||||||
|
d.Templates = templates
|
||||||
d.Name = pb.Name
|
d.Name = pb.Name
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -300,7 +366,7 @@ func UnmarshalUser(data []byte, u *chronograf.User) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalUser decodes a user from binary protobuf data.
|
// UnmarshalUserPB decodes a user from binary protobuf data.
|
||||||
// We are ignoring the password for now.
|
// We are ignoring the password for now.
|
||||||
func UnmarshalUserPB(data []byte, u *User) error {
|
func UnmarshalUserPB(data []byte, u *User) error {
|
||||||
if err := proto.Unmarshal(data, u); err != nil {
|
if err := proto.Unmarshal(data, u); err != nil {
|
||||||
|
|
|
@ -12,6 +12,9 @@ It has these top-level messages:
|
||||||
Source
|
Source
|
||||||
Dashboard
|
Dashboard
|
||||||
DashboardCell
|
DashboardCell
|
||||||
|
Template
|
||||||
|
TemplateValue
|
||||||
|
TemplateQuery
|
||||||
Server
|
Server
|
||||||
Layout
|
Layout
|
||||||
Cell
|
Cell
|
||||||
|
@ -56,9 +59,10 @@ func (*Source) ProtoMessage() {}
|
||||||
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
|
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
|
||||||
|
|
||||||
type Dashboard struct {
|
type Dashboard struct {
|
||||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||||
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
|
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
|
||||||
|
Templates []*Template `protobuf:"bytes,4,rep,name=templates" json:"templates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Dashboard) Reset() { *m = Dashboard{} }
|
func (m *Dashboard) Reset() { *m = Dashboard{} }
|
||||||
|
@ -73,6 +77,13 @@ func (m *Dashboard) GetCells() []*DashboardCell {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Dashboard) GetTemplates() []*Template {
|
||||||
|
if m != nil {
|
||||||
|
return m.Templates
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardCell struct {
|
type DashboardCell struct {
|
||||||
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
||||||
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
|
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
|
||||||
|
@ -96,6 +107,59 @@ func (m *DashboardCell) GetQueries() []*Query {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
|
TempVar string `protobuf:"bytes,2,opt,name=temp_var,json=tempVar,proto3" json:"temp_var,omitempty"`
|
||||||
|
Values []*TemplateValue `protobuf:"bytes,3,rep,name=values" json:"values,omitempty"`
|
||||||
|
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
|
Label string `protobuf:"bytes,5,opt,name=label,proto3" json:"label,omitempty"`
|
||||||
|
Query *TemplateQuery `protobuf:"bytes,6,opt,name=query" json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Template) Reset() { *m = Template{} }
|
||||||
|
func (m *Template) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Template) ProtoMessage() {}
|
||||||
|
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
||||||
|
|
||||||
|
func (m *Template) GetValues() []*TemplateValue {
|
||||||
|
if m != nil {
|
||||||
|
return m.Values
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Template) GetQuery() *TemplateQuery {
|
||||||
|
if m != nil {
|
||||||
|
return m.Query
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateValue struct {
|
||||||
|
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
|
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TemplateValue) Reset() { *m = TemplateValue{} }
|
||||||
|
func (m *TemplateValue) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*TemplateValue) ProtoMessage() {}
|
||||||
|
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||||
|
|
||||||
|
type TemplateQuery struct {
|
||||||
|
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
|
||||||
|
Db string `protobuf:"bytes,2,opt,name=db,proto3" json:"db,omitempty"`
|
||||||
|
Rp string `protobuf:"bytes,3,opt,name=rp,proto3" json:"rp,omitempty"`
|
||||||
|
Measurement string `protobuf:"bytes,4,opt,name=measurement,proto3" json:"measurement,omitempty"`
|
||||||
|
TagKey string `protobuf:"bytes,5,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"`
|
||||||
|
FieldKey string `protobuf:"bytes,6,opt,name=field_key,json=fieldKey,proto3" json:"field_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TemplateQuery) Reset() { *m = TemplateQuery{} }
|
||||||
|
func (m *TemplateQuery) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*TemplateQuery) ProtoMessage() {}
|
||||||
|
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||||
|
@ -109,7 +173,7 @@ type Server struct {
|
||||||
func (m *Server) Reset() { *m = Server{} }
|
func (m *Server) Reset() { *m = Server{} }
|
||||||
func (m *Server) String() string { return proto.CompactTextString(m) }
|
func (m *Server) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Server) ProtoMessage() {}
|
func (*Server) ProtoMessage() {}
|
||||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||||
|
|
||||||
type Layout struct {
|
type Layout struct {
|
||||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
|
@ -122,7 +186,7 @@ type Layout struct {
|
||||||
func (m *Layout) Reset() { *m = Layout{} }
|
func (m *Layout) Reset() { *m = Layout{} }
|
||||||
func (m *Layout) String() string { return proto.CompactTextString(m) }
|
func (m *Layout) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Layout) ProtoMessage() {}
|
func (*Layout) ProtoMessage() {}
|
||||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||||
|
|
||||||
func (m *Layout) GetCells() []*Cell {
|
func (m *Layout) GetCells() []*Cell {
|
||||||
if m != nil {
|
if m != nil {
|
||||||
|
@ -147,7 +211,7 @@ type Cell struct {
|
||||||
func (m *Cell) Reset() { *m = Cell{} }
|
func (m *Cell) Reset() { *m = Cell{} }
|
||||||
func (m *Cell) String() string { return proto.CompactTextString(m) }
|
func (m *Cell) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Cell) ProtoMessage() {}
|
func (*Cell) ProtoMessage() {}
|
||||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||||
|
|
||||||
func (m *Cell) GetQueries() []*Query {
|
func (m *Cell) GetQueries() []*Query {
|
||||||
if m != nil {
|
if m != nil {
|
||||||
|
@ -169,7 +233,7 @@ type Query struct {
|
||||||
func (m *Query) Reset() { *m = Query{} }
|
func (m *Query) Reset() { *m = Query{} }
|
||||||
func (m *Query) String() string { return proto.CompactTextString(m) }
|
func (m *Query) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Query) ProtoMessage() {}
|
func (*Query) ProtoMessage() {}
|
||||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||||
|
|
||||||
func (m *Query) GetRange() *Range {
|
func (m *Query) GetRange() *Range {
|
||||||
if m != nil {
|
if m != nil {
|
||||||
|
@ -186,7 +250,7 @@ type Range struct {
|
||||||
func (m *Range) Reset() { *m = Range{} }
|
func (m *Range) Reset() { *m = Range{} }
|
||||||
func (m *Range) String() string { return proto.CompactTextString(m) }
|
func (m *Range) String() string { return proto.CompactTextString(m) }
|
||||||
func (*Range) ProtoMessage() {}
|
func (*Range) ProtoMessage() {}
|
||||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||||
|
|
||||||
type AlertRule struct {
|
type AlertRule struct {
|
||||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
|
@ -198,7 +262,7 @@ type AlertRule struct {
|
||||||
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
||||||
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
|
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
|
||||||
func (*AlertRule) ProtoMessage() {}
|
func (*AlertRule) ProtoMessage() {}
|
||||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||||
|
@ -208,12 +272,15 @@ type User struct {
|
||||||
func (m *User) Reset() { *m = User{} }
|
func (m *User) Reset() { *m = User{} }
|
||||||
func (m *User) String() string { return proto.CompactTextString(m) }
|
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||||
func (*User) ProtoMessage() {}
|
func (*User) ProtoMessage() {}
|
||||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||||
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
||||||
|
proto.RegisterType((*Template)(nil), "internal.Template")
|
||||||
|
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
|
||||||
|
proto.RegisterType((*TemplateQuery)(nil), "internal.TemplateQuery")
|
||||||
proto.RegisterType((*Server)(nil), "internal.Server")
|
proto.RegisterType((*Server)(nil), "internal.Server")
|
||||||
proto.RegisterType((*Layout)(nil), "internal.Layout")
|
proto.RegisterType((*Layout)(nil), "internal.Layout")
|
||||||
proto.RegisterType((*Cell)(nil), "internal.Cell")
|
proto.RegisterType((*Cell)(nil), "internal.Cell")
|
||||||
|
@ -226,47 +293,59 @@ func init() {
|
||||||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||||
|
|
||||||
var fileDescriptorInternal = []byte{
|
var fileDescriptorInternal = []byte{
|
||||||
// 670 bytes of a gzipped FileDescriptorProto
|
// 858 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xcd, 0x6e, 0xd3, 0x4a,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
|
||||||
0x14, 0xd6, 0xc4, 0x76, 0x7e, 0x4e, 0x7b, 0x7b, 0xaf, 0x46, 0x57, 0x30, 0x62, 0x15, 0x59, 0x20,
|
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xee, 0x16, 0x34, 0x5a, 0xb1, 0x06, 0x6e, 0x22, 0x0b, 0xa4,
|
||||||
0x05, 0x24, 0xba, 0xa0, 0x4f, 0x90, 0xd6, 0x12, 0x0a, 0xb4, 0xa5, 0x4c, 0x5a, 0x58, 0x81, 0x34,
|
0x82, 0x44, 0x41, 0xec, 0x13, 0xb4, 0xb5, 0x84, 0x42, 0xbb, 0x4b, 0x99, 0xb4, 0xe5, 0x0a, 0xad,
|
||||||
0x4d, 0x4f, 0x1a, 0x0b, 0xc7, 0x36, 0x63, 0xbb, 0xa9, 0x5f, 0x81, 0x87, 0x60, 0xc5, 0x8a, 0x25,
|
0x26, 0xc9, 0x49, 0x6b, 0xed, 0x24, 0x36, 0x63, 0xbb, 0x59, 0xbf, 0x02, 0x57, 0x3c, 0x01, 0x12,
|
||||||
0xaf, 0xc2, 0x0b, 0xa1, 0x33, 0x33, 0x76, 0x52, 0x28, 0xa8, 0x2b, 0x76, 0xe7, 0x3b, 0xc7, 0x39,
|
0x12, 0x57, 0x5c, 0xf2, 0x02, 0x3c, 0x04, 0x2f, 0x84, 0xce, 0xcc, 0xf8, 0x27, 0x6c, 0x41, 0x7b,
|
||||||
0x3f, 0xdf, 0xf7, 0x4d, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
|
0xb5, 0x77, 0xf3, 0x9d, 0x33, 0xf9, 0xe6, 0xfc, 0x7c, 0x9f, 0x03, 0x87, 0xd9, 0xb6, 0x42, 0xbd,
|
||||||
0xef, 0x37, 0x38, 0xfc, 0xd4, 0x81, 0xee, 0x34, 0xab, 0xf4, 0x0c, 0xf9, 0x0e, 0x74, 0x26, 0x91,
|
0x95, 0xea, 0xb8, 0xd0, 0x79, 0x95, 0xf3, 0xb0, 0xc5, 0xc9, 0xcf, 0x23, 0x18, 0xcf, 0xf3, 0x5a,
|
||||||
0x60, 0x43, 0x36, 0xf2, 0x64, 0x67, 0x12, 0x71, 0x0e, 0xfe, 0xb1, 0x5a, 0xa2, 0xe8, 0x0c, 0xd9,
|
0x2f, 0x91, 0x1f, 0xc2, 0x68, 0x96, 0xc6, 0x6c, 0xca, 0x8e, 0x3c, 0x31, 0x9a, 0xa5, 0x9c, 0x83,
|
||||||
0x68, 0x20, 0x4d, 0x4c, 0xb9, 0xd3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0xf4, 0xcf,
|
0xff, 0x42, 0x6e, 0x30, 0x1e, 0x4d, 0xd9, 0x51, 0x24, 0xcc, 0x99, 0x62, 0x57, 0x4d, 0x81, 0xb1,
|
||||||
0x0a, 0xea, 0xb6, 0x44, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xa2, 0x8a, 0x62, 0x95, 0xe9, 0x0b,
|
0x67, 0x63, 0x74, 0xe6, 0x1f, 0x41, 0x78, 0x5d, 0x12, 0xdb, 0x06, 0x63, 0xdf, 0xc4, 0x3b, 0x4c,
|
||||||
0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xc9, 0x43, 0xd1, 0x35, 0x69, 0x0a, 0xb9, 0x80,
|
0xb9, 0x4b, 0x59, 0x96, 0xbb, 0x5c, 0xaf, 0xe2, 0xc0, 0xe6, 0x5a, 0xcc, 0xdf, 0x07, 0xef, 0x5a,
|
||||||
0x5e, 0x84, 0x73, 0x55, 0x25, 0xa5, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x1b, 0x48, 0x7d, 0x4e, 0x31,
|
0x5c, 0xc4, 0x63, 0x13, 0xa6, 0x23, 0x8f, 0x61, 0x92, 0xe2, 0x5a, 0xd6, 0xaa, 0x8a, 0x27, 0x53,
|
||||||
0xc1, 0x4b, 0xad, 0xe6, 0xa2, 0x6f, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa4, 0x05, 0xce, 0x2a,
|
0x76, 0x14, 0x8a, 0x16, 0x12, 0xcf, 0x15, 0x2a, 0xbc, 0xd5, 0x72, 0x1d, 0x87, 0x96, 0xa7, 0xc5,
|
||||||
0x8d, 0xd3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0x3c, 0xaf, 0xc5, 0xc0, 0x34, 0xb8, 0xa5, 0x42, 0x53,
|
0xfc, 0x18, 0xf8, 0x6c, 0x5b, 0xe2, 0xb2, 0xd6, 0x38, 0x7f, 0x95, 0x15, 0x37, 0xa8, 0xb3, 0x75,
|
||||||
0x8e, 0xb0, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x10, 0xa9, 0x62, 0x71, 0x9e,
|
0x13, 0x47, 0x86, 0xe0, 0x81, 0x0c, 0xbd, 0xf2, 0x1c, 0x2b, 0x49, 0x6f, 0x83, 0xa1, 0x6a, 0x61,
|
||||||
0x29, 0x7d, 0x71, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc3, 0x24, 0x29, 0x84, 0x37, 0xf4, 0x46, 0x5b,
|
0xf2, 0x0b, 0x83, 0x28, 0x95, 0xe5, 0xdd, 0x22, 0x97, 0x7a, 0xf5, 0x56, 0xf3, 0xf8, 0x02, 0x82,
|
||||||
0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x30, 0x49, 0xa4, 0xfd, 0x2a, 0xfc, 0xca, 0xe0,
|
0x25, 0x2a, 0x55, 0xc6, 0xde, 0xd4, 0x3b, 0x3a, 0xf8, 0xfa, 0xe9, 0x71, 0x37, 0xe8, 0x8e, 0xe7,
|
||||||
0x9f, 0x1b, 0x05, 0xbe, 0x0d, 0xec, 0xda, 0xcc, 0x08, 0x24, 0xbb, 0x26, 0x54, 0x9b, 0xfe, 0x81,
|
0x0c, 0x95, 0x12, 0xf6, 0x16, 0xff, 0x0a, 0xa2, 0x0a, 0x37, 0x85, 0x92, 0x15, 0x96, 0xb1, 0x6f,
|
||||||
0x64, 0x35, 0xa1, 0x95, 0x21, 0x3a, 0x90, 0x6c, 0x45, 0x68, 0x61, 0xe8, 0x0d, 0x24, 0x5b, 0xf0,
|
0x7e, 0xc2, 0xfb, 0x9f, 0x5c, 0xb9, 0x94, 0xe8, 0x2f, 0x25, 0x7f, 0x30, 0x78, 0xbc, 0x47, 0xc5,
|
||||||
0xc7, 0xd0, 0xfb, 0x58, 0xa1, 0x8e, 0xb1, 0x10, 0x81, 0x19, 0xfd, 0xef, 0x7a, 0xf4, 0xeb, 0x0a,
|
0x1f, 0x01, 0x7b, 0x6d, 0xaa, 0x0a, 0x04, 0x7b, 0x4d, 0xa8, 0x31, 0x15, 0x05, 0x82, 0x35, 0x84,
|
||||||
0x75, 0x2d, 0x9b, 0x3a, 0xed, 0x6d, 0xa4, 0xb1, 0x3c, 0x9b, 0x98, 0x72, 0x25, 0xc9, 0xd8, 0xb3,
|
0x76, 0x66, 0x37, 0x81, 0x60, 0x3b, 0x42, 0x77, 0x66, 0x23, 0x81, 0x60, 0x77, 0xfc, 0x33, 0x98,
|
||||||
0x39, 0x8a, 0xdd, 0xbd, 0x96, 0xdc, 0xce, 0x24, 0x0a, 0xbf, 0x30, 0xe8, 0x4e, 0x51, 0x5f, 0xa1,
|
0xfc, 0x54, 0xa3, 0xce, 0xb0, 0x8c, 0x03, 0xf3, 0xf2, 0x7b, 0xfd, 0xcb, 0xdf, 0xd7, 0xa8, 0x1b,
|
||||||
0xbe, 0x13, 0x15, 0x9b, 0x2e, 0xf0, 0xfe, 0xe0, 0x02, 0xff, 0x76, 0x17, 0x04, 0x6b, 0x17, 0xfc,
|
0xd1, 0xe6, 0xa9, 0x53, 0xb3, 0x4d, 0xbb, 0x1a, 0x73, 0xa6, 0x58, 0x45, 0x9b, 0x9f, 0xd8, 0x18,
|
||||||
0x0f, 0xc1, 0x54, 0xcf, 0x26, 0x91, 0xd9, 0xd8, 0x93, 0x16, 0xf0, 0x7b, 0xd0, 0x1d, 0xcf, 0xca,
|
0x9d, 0xdd, 0x84, 0xec, 0x3e, 0x46, 0xb3, 0x34, 0xf9, 0x8b, 0xd1, 0x9a, 0x6c, 0xe9, 0x83, 0xf1,
|
||||||
0xf8, 0x0a, 0x9d, 0x35, 0x1c, 0x0a, 0x3f, 0x33, 0xe8, 0x1e, 0xaa, 0x3a, 0xab, 0xca, 0x8d, 0x35,
|
0x99, 0x24, 0xff, 0x10, 0x42, 0x6a, 0xeb, 0xe5, 0xbd, 0xd4, 0x6e, 0x84, 0x13, 0xc2, 0x37, 0x52,
|
||||||
0xcd, 0x05, 0x7c, 0x08, 0x5b, 0xe3, 0x3c, 0x4f, 0xe2, 0x99, 0x2a, 0xe3, 0x2c, 0x75, 0xdb, 0x6e,
|
0xf3, 0x2f, 0x61, 0x7c, 0x2f, 0x55, 0x8d, 0x0f, 0x8c, 0xb1, 0xa5, 0xbb, 0xa1, 0xbc, 0x70, 0xd7,
|
||||||
0xa6, 0xe8, 0x8b, 0x23, 0x54, 0x45, 0xa5, 0x71, 0x89, 0x69, 0xe9, 0xf6, 0xde, 0x4c, 0xf1, 0x87,
|
0xba, 0x62, 0xfc, 0x41, 0x31, 0x4f, 0x20, 0x50, 0x72, 0x81, 0xca, 0xe9, 0xcc, 0x02, 0x5a, 0x10,
|
||||||
0x10, 0x1c, 0x18, 0x85, 0x7d, 0x43, 0xf3, 0xce, 0x9a, 0x66, 0x2b, 0xac, 0x29, 0xd2, 0x81, 0xe3,
|
0x75, 0xd5, 0x98, 0x5e, 0x1e, 0x64, 0xb6, 0xbd, 0xdb, 0x5b, 0xc9, 0x35, 0x3c, 0xde, 0x7b, 0xb1,
|
||||||
0xaa, 0xcc, 0xe6, 0x49, 0xb6, 0x32, 0x97, 0xf4, 0x65, 0x8b, 0xc3, 0xef, 0x0c, 0xfc, 0xbf, 0xa5,
|
0x7b, 0x89, 0xed, 0xbf, 0x64, 0xea, 0x70, 0x6d, 0x58, 0x40, 0x12, 0x2d, 0x51, 0xe1, 0xb2, 0xc2,
|
||||||
0xf5, 0x36, 0xb0, 0xd8, 0x09, 0xcd, 0xe2, 0x56, 0xf9, 0xde, 0x86, 0xf2, 0x02, 0x7a, 0xb5, 0x56,
|
0x95, 0x59, 0x41, 0x28, 0x3a, 0x9c, 0xfc, 0xc6, 0x7a, 0x5e, 0xf3, 0x1e, 0x89, 0x70, 0x99, 0x6f,
|
||||||
0xe9, 0x25, 0x16, 0xa2, 0x3f, 0xf4, 0x46, 0x9e, 0x6c, 0xa0, 0xa9, 0x24, 0xea, 0x1c, 0x93, 0x42,
|
0x36, 0x72, 0xbb, 0x72, 0xd4, 0x2d, 0xa4, 0xb9, 0xad, 0x16, 0x8e, 0x7a, 0xb4, 0x5a, 0x10, 0xd6,
|
||||||
0x0c, 0x86, 0x1e, 0x3d, 0x0b, 0x07, 0x5b, 0xb7, 0xc0, 0xda, 0x2d, 0xe1, 0x37, 0x06, 0x81, 0x19,
|
0x85, 0x33, 0xdc, 0x48, 0x17, 0x7c, 0x0a, 0x07, 0x1b, 0x94, 0x65, 0xad, 0x71, 0x83, 0xdb, 0xca,
|
||||||
0x4e, 0xbf, 0x3b, 0xc8, 0x96, 0x4b, 0x95, 0x5e, 0x38, 0xea, 0x1b, 0x48, 0x7a, 0x44, 0xfb, 0x8e,
|
0x8d, 0x60, 0x18, 0xe2, 0x4f, 0x61, 0x52, 0xc9, 0xdb, 0x97, 0xaf, 0xb0, 0x71, 0xb3, 0x18, 0x57,
|
||||||
0xf6, 0x4e, 0xb4, 0x4f, 0x58, 0x9e, 0x38, 0x92, 0x3b, 0xf2, 0x84, 0x58, 0x7b, 0xae, 0xb3, 0x2a,
|
0xf2, 0xf6, 0x1c, 0x1b, 0xfe, 0x31, 0x44, 0xeb, 0x0c, 0xd5, 0xca, 0xa4, 0xec, 0x72, 0x43, 0x13,
|
||||||
0xdf, 0xaf, 0x2d, 0xbd, 0x03, 0xd9, 0x62, 0x92, 0xfb, 0xed, 0x02, 0xb5, 0xbb, 0x79, 0x20, 0x1d,
|
0x38, 0xc7, 0x26, 0xf9, 0x9d, 0xc1, 0x78, 0x8e, 0xfa, 0x1e, 0xf5, 0x5b, 0x29, 0x7f, 0xe8, 0x7a,
|
||||||
0x22, 0x73, 0x1c, 0xd2, 0x56, 0xee, 0x4a, 0x0b, 0xf8, 0x23, 0x08, 0x24, 0x5d, 0x61, 0x4e, 0xbd,
|
0xef, 0x7f, 0x5c, 0xef, 0x3f, 0xec, 0xfa, 0xa0, 0x77, 0xfd, 0x13, 0x08, 0xe6, 0x7a, 0x39, 0x4b,
|
||||||
0x41, 0x90, 0x49, 0x4b, 0x5b, 0x0d, 0xf7, 0xdc, 0x67, 0xd4, 0xe5, 0x2c, 0xcf, 0x51, 0x3b, 0x4f,
|
0x4d, 0x45, 0x9e, 0xb0, 0x80, 0x7f, 0x00, 0xe3, 0x93, 0x65, 0x95, 0xdd, 0xa3, 0xfb, 0x14, 0x38,
|
||||||
0x5b, 0x60, 0x7a, 0x67, 0x2b, 0xd4, 0x66, 0x65, 0x4f, 0x5a, 0x10, 0xbe, 0x83, 0xc1, 0x38, 0x41,
|
0x94, 0xfc, 0xca, 0x60, 0x7c, 0x21, 0x9b, 0xbc, 0xae, 0xde, 0x50, 0xd8, 0x14, 0x0e, 0x4e, 0x8a,
|
||||||
0x5d, 0xca, 0x2a, 0xc1, 0x5f, 0x2c, 0xc6, 0xc1, 0x7f, 0x31, 0x7d, 0x75, 0xdc, 0xbc, 0x04, 0x8a,
|
0x42, 0x65, 0x4b, 0x59, 0x65, 0xf9, 0xd6, 0x55, 0x3b, 0x0c, 0xd1, 0x8d, 0xe7, 0x83, 0xd9, 0xd9,
|
||||||
0xd7, 0xfe, 0xf5, 0x7e, 0xf2, 0xef, 0x4b, 0x95, 0xab, 0x49, 0x64, 0x84, 0xf5, 0xa4, 0x43, 0xe1,
|
0xba, 0x87, 0x21, 0xfe, 0x09, 0x04, 0x67, 0xc6, 0xd0, 0xd6, 0x9d, 0x87, 0xbd, 0x5e, 0xac, 0x8f,
|
||||||
0x13, 0xf0, 0xe9, 0x9d, 0x6c, 0x74, 0xf6, 0x7f, 0xf7, 0xc6, 0xce, 0xbb, 0xe6, 0xdf, 0x7b, 0xef,
|
0x4d, 0x92, 0x1a, 0x3c, 0xa9, 0xab, 0x7c, 0xad, 0xf2, 0x9d, 0xe9, 0x24, 0x14, 0x1d, 0x4e, 0xfe,
|
||||||
0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0xbe, 0xb0, 0xc3, 0xcf, 0x05, 0x00, 0x00,
|
0x66, 0xe0, 0xbf, 0x2b, 0xa3, 0x3e, 0x02, 0x96, 0xb9, 0x45, 0xb2, 0xac, 0xb3, 0xed, 0x64, 0x60,
|
||||||
|
0xdb, 0x18, 0x26, 0x8d, 0x96, 0xdb, 0x5b, 0x2c, 0xe3, 0x70, 0xea, 0x1d, 0x79, 0xa2, 0x85, 0x26,
|
||||||
|
0x63, 0x3c, 0x52, 0xc6, 0xd1, 0xd4, 0x23, 0x05, 0x3a, 0xd8, 0x69, 0x1e, 0x7a, 0xcd, 0x27, 0x7f,
|
||||||
|
0x32, 0x08, 0x3a, 0xe5, 0x9e, 0xed, 0x2b, 0xf7, 0xac, 0x57, 0x6e, 0x7a, 0xda, 0x2a, 0x37, 0x3d,
|
||||||
|
0x25, 0x2c, 0x2e, 0x5b, 0xe5, 0x8a, 0x4b, 0x9a, 0xda, 0x37, 0x3a, 0xaf, 0x8b, 0xd3, 0xc6, 0x8e,
|
||||||
|
0x37, 0x12, 0x1d, 0xa6, 0x75, 0xff, 0x70, 0x87, 0xda, 0xf5, 0x1c, 0x09, 0x87, 0x48, 0x1c, 0x17,
|
||||||
|
0xc6, 0xd5, 0xb6, 0x4b, 0x0b, 0xf8, 0xa7, 0x10, 0x08, 0xea, 0xc2, 0xb4, 0xba, 0x37, 0x20, 0x13,
|
||||||
|
0x16, 0x36, 0x9b, 0x3c, 0x73, 0xd7, 0x88, 0xe5, 0xba, 0x28, 0x50, 0x3b, 0x4d, 0x5b, 0x60, 0xb8,
|
||||||
|
0xf3, 0x1d, 0xda, 0xcf, 0x91, 0x27, 0x2c, 0x48, 0x7e, 0x84, 0xe8, 0x44, 0xa1, 0xae, 0x44, 0xad,
|
||||||
|
0xde, 0xfc, 0x88, 0x71, 0xf0, 0xbf, 0x9d, 0x7f, 0xf7, 0xa2, 0x75, 0x02, 0x9d, 0x7b, 0xfd, 0x7a,
|
||||||
|
0xff, 0xd2, 0xef, 0xb9, 0x2c, 0xe4, 0x2c, 0x35, 0x8b, 0xf5, 0x84, 0x43, 0xc9, 0xe7, 0xe0, 0x93,
|
||||||
|
0x4f, 0x06, 0xcc, 0xfe, 0x7f, 0x79, 0x6c, 0x31, 0x36, 0xff, 0xd6, 0xcf, 0xfe, 0x09, 0x00, 0x00,
|
||||||
|
0xff, 0xff, 0xa7, 0xc6, 0x53, 0x22, 0xbf, 0x07, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ message Dashboard {
|
||||||
int64 ID = 1; // ID is the unique ID of the dashboard
|
int64 ID = 1; // ID is the unique ID of the dashboard
|
||||||
string Name = 2; // Name is the user-defined name of the dashboard
|
string Name = 2; // Name is the user-defined name of the dashboard
|
||||||
repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard
|
repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard
|
||||||
|
repeated Template templates = 4; // Templates replace template variables within InfluxQL
|
||||||
}
|
}
|
||||||
|
|
||||||
message DashboardCell {
|
message DashboardCell {
|
||||||
|
@ -31,6 +32,30 @@ message DashboardCell {
|
||||||
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Template {
|
||||||
|
string ID = 1; // ID is the unique ID associated with this template
|
||||||
|
string temp_var = 2;
|
||||||
|
repeated TemplateValue values = 3;
|
||||||
|
string type = 4; // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
|
||||||
|
string label = 5; // Label is a user-facing description of the Template
|
||||||
|
TemplateQuery query = 6; // Query is used to generate the choices for a template
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemplateValue {
|
||||||
|
string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
|
||||||
|
string value = 2; // Value is the specific value used to replace a template in an InfluxQL query
|
||||||
|
bool selected = 3; // Selected states that this variable has been picked to use for replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemplateQuery {
|
||||||
|
string command = 1; // Command is the query itself
|
||||||
|
string db = 2; // DB the database for the query (optional)
|
||||||
|
string rp = 3; // RP is a retention policy and optional;
|
||||||
|
string measurement = 4; // Measurement is the optinally selected measurement for the query
|
||||||
|
string tag_key = 5; // TagKey is the optionally selected tag key for the query
|
||||||
|
string field_key = 6; // FieldKey is the optionally selected field key for the query
|
||||||
|
}
|
||||||
|
|
||||||
message Server {
|
message Server {
|
||||||
int64 ID = 1; // ID is the unique ID of the server
|
int64 ID = 1; // ID is the unique ID of the server
|
||||||
string Name = 2; // Name is the user-defined name for the server
|
string Name = 2; // Name is the user-defined name for the server
|
||||||
|
|
|
@ -123,15 +123,59 @@ type Range struct {
|
||||||
Lower int64 `json:"lower"` // Lower is the lower bound
|
Lower int64 `json:"lower"` // Lower is the lower bound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateValue is a value use to replace a template in an InfluxQL query
|
||||||
|
type TemplateValue struct {
|
||||||
|
Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query
|
||||||
|
Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
|
||||||
|
Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateVar is a named variable within an InfluxQL query to be replaced with Values
|
||||||
|
type TemplateVar struct {
|
||||||
|
Var string `json:"tempVar"` // Var is the string to replace within InfluxQL
|
||||||
|
Values []TemplateValue `json:"values"` // Values are the replacement values within InfluxQL
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts the template variable into a correct InfluxQL string based
|
||||||
|
// on its type
|
||||||
|
func (t TemplateVar) String() string {
|
||||||
|
if len(t.Values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t.Values[0].Type {
|
||||||
|
case "tagKey", "fieldKey", "measurement", "database":
|
||||||
|
return `"` + t.Values[0].Value + `"`
|
||||||
|
case "tagValue":
|
||||||
|
return `'` + t.Values[0].Value + `'`
|
||||||
|
case "csv", "constant":
|
||||||
|
return t.Values[0].Value
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateID is the unique ID used to identify a template
|
||||||
|
type TemplateID string
|
||||||
|
|
||||||
|
// Template represents a series of choices to replace TemplateVars within InfluxQL
|
||||||
|
type Template struct {
|
||||||
|
TemplateVar
|
||||||
|
ID TemplateID `json:"id"` // ID is the unique ID associated with this template
|
||||||
|
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
|
||||||
|
Label string `json:"label"` // Label is a user-facing description of the Template
|
||||||
|
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
|
||||||
|
}
|
||||||
|
|
||||||
// Query retrieves a Response from a TimeSeries.
|
// Query retrieves a Response from a TimeSeries.
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Command string `json:"query"` // Command is the query itself
|
Command string `json:"query"` // Command is the query itself
|
||||||
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
|
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
|
||||||
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
|
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
|
||||||
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
|
TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query
|
||||||
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
|
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
|
||||||
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
|
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
|
||||||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardQuery includes state for the query builder. This is a transition
|
// DashboardQuery includes state for the query builder. This is a transition
|
||||||
|
@ -143,6 +187,16 @@ type DashboardQuery struct {
|
||||||
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
|
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateQuery is used to retrieve choices for template replacement
|
||||||
|
type TemplateQuery struct {
|
||||||
|
Command string `json:"influxql"` // Command is the query itself
|
||||||
|
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
|
||||||
|
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
|
||||||
|
Measurement string `json:"measurement"` // Measurement is the optinally selected measurement for the query
|
||||||
|
TagKey string `json:"tagKey"` // TagKey is the optionally selected tag key for the query
|
||||||
|
FieldKey string `json:"fieldKey"` // FieldKey is the optionally selected field key for the query
|
||||||
|
}
|
||||||
|
|
||||||
// Response is the result of a query against a TimeSeries
|
// Response is the result of a query against a TimeSeries
|
||||||
type Response interface {
|
type Response interface {
|
||||||
MarshalJSON() ([]byte, error)
|
MarshalJSON() ([]byte, error)
|
||||||
|
@ -374,9 +428,10 @@ type DashboardID int
|
||||||
|
|
||||||
// Dashboard represents all visual and query data for a dashboard
|
// Dashboard represents all visual and query data for a dashboard
|
||||||
type Dashboard struct {
|
type Dashboard struct {
|
||||||
ID DashboardID `json:"id"`
|
ID DashboardID `json:"id"`
|
||||||
Cells []DashboardCell `json:"cells"`
|
Cells []DashboardCell `json:"cells"`
|
||||||
Name string `json:"name"`
|
Templates []Template `json:"templates"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardCell holds visual and query information for a cell
|
// DashboardCell holds visual and query information for a cell
|
||||||
|
|
|
@ -68,17 +68,20 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
command := q.Command
|
||||||
c.Logger.
|
if len(q.TemplateVars) > 0 {
|
||||||
|
command = TemplateReplace(q.Command, q.TemplateVars)
|
||||||
|
}
|
||||||
|
logs := c.Logger.
|
||||||
WithField("component", "proxy").
|
WithField("component", "proxy").
|
||||||
WithField("host", req.Host).
|
WithField("host", req.Host).
|
||||||
WithField("command", q.Command).
|
WithField("command", command).
|
||||||
WithField("db", q.DB).
|
WithField("db", q.DB).
|
||||||
WithField("rp", q.RP).
|
WithField("rp", q.RP)
|
||||||
Debug("query")
|
logs.Debug("query")
|
||||||
|
|
||||||
params := req.URL.Query()
|
params := req.URL.Query()
|
||||||
params.Set("q", q.Command)
|
params.Set("q", command)
|
||||||
params.Set("db", q.DB)
|
params.Set("db", q.DB)
|
||||||
params.Set("rp", q.RP)
|
params.Set("rp", q.RP)
|
||||||
params.Set("epoch", "ms")
|
params.Set("epoch", "ms")
|
||||||
|
@ -111,13 +114,7 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
||||||
|
|
||||||
// If we got a valid decode error, send that back
|
// If we got a valid decode error, send that back
|
||||||
if decErr != nil {
|
if decErr != nil {
|
||||||
c.Logger.
|
logs.WithField("influx_status", resp.StatusCode).
|
||||||
WithField("component", "proxy").
|
|
||||||
WithField("host", req.Host).
|
|
||||||
WithField("command", q.Command).
|
|
||||||
WithField("db", q.DB).
|
|
||||||
WithField("rp", q.RP).
|
|
||||||
WithField("influx_status", resp.StatusCode).
|
|
||||||
Error("Error parsing results from influxdb: err:", decErr)
|
Error("Error parsing results from influxdb: err:", decErr)
|
||||||
return nil, decErr
|
return nil, decErr
|
||||||
}
|
}
|
||||||
|
@ -125,12 +122,7 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
||||||
// If we don't have an error in our json response, and didn't get statusOK
|
// If we don't have an error in our json response, and didn't get statusOK
|
||||||
// then send back an error
|
// then send back an error
|
||||||
if resp.StatusCode != http.StatusOK && response.Err != "" {
|
if resp.StatusCode != http.StatusOK && response.Err != "" {
|
||||||
c.Logger.
|
logs.
|
||||||
WithField("component", "proxy").
|
|
||||||
WithField("host", req.Host).
|
|
||||||
WithField("command", q.Command).
|
|
||||||
WithField("db", q.DB).
|
|
||||||
WithField("rp", q.RP).
|
|
||||||
WithField("influx_status", resp.StatusCode).
|
WithField("influx_status", resp.StatusCode).
|
||||||
Error("Received non-200 response from influxdb")
|
Error("Received non-200 response from influxdb")
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ func Test_Influx_HTTPS_Failure(t *testing.T) {
|
||||||
func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
called := false
|
called := false
|
||||||
|
q := ""
|
||||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
rw.Write([]byte(`{}`))
|
rw.Write([]byte(`{}`))
|
||||||
|
@ -89,6 +90,8 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
||||||
if path := r.URL.Path; path != "/query" {
|
if path := r.URL.Path; path != "/query" {
|
||||||
t.Error("Expected the path to contain `/query` but was", path)
|
t.Error("Expected the path to contain `/query` but was", path)
|
||||||
}
|
}
|
||||||
|
values := r.URL.Query()
|
||||||
|
q = values.Get("q")
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
@ -118,6 +121,34 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
||||||
if called == false {
|
if called == false {
|
||||||
t.Error("Expected http request to Influx but there was none")
|
t.Error("Expected http request to Influx but there was none")
|
||||||
}
|
}
|
||||||
|
called = false
|
||||||
|
q = ""
|
||||||
|
query = chronograf.Query{
|
||||||
|
Command: "select $field from cpu",
|
||||||
|
TemplateVars: []chronograf.TemplateVar{
|
||||||
|
{
|
||||||
|
Var: "$field",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Value: "usage_user",
|
||||||
|
Type: "fieldKey",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = series.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error but was", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if called == false {
|
||||||
|
t.Error("Expected http request to Influx but there was none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if q != `select "usage_user" from cpu` {
|
||||||
|
t.Errorf("Unexpected query: %s", q)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package influx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TemplateReplace replaces templates with values within the query string
|
||||||
|
func TemplateReplace(query string, templates []chronograf.TemplateVar) string {
|
||||||
|
replacements := []string{}
|
||||||
|
for _, v := range templates {
|
||||||
|
newVal := v.String()
|
||||||
|
if newVal != "" {
|
||||||
|
replacements = append(replacements, v.Var, newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replacer := strings.NewReplacer(replacements...)
|
||||||
|
replaced := replacer.Replace(query)
|
||||||
|
return replaced
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package influx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTemplateReplace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
vars []chronograf.TemplateVar
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "select with parameters",
|
||||||
|
query: "$METHOD field1, $field FROM $measurement WHERE temperature > $temperature",
|
||||||
|
vars: []chronograf.TemplateVar{
|
||||||
|
{
|
||||||
|
Var: "$temperature",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "csv",
|
||||||
|
Value: "10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Var: "$field",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "fieldKey",
|
||||||
|
Value: "field2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Var: "$METHOD",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "csv",
|
||||||
|
Value: "SELECT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Var: "$measurement",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "csv",
|
||||||
|
Value: `"cpu"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `SELECT field1, "field2" FROM "cpu" WHERE temperature > 10`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with parameters and aggregates",
|
||||||
|
query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`,
|
||||||
|
vars: []chronograf.TemplateVar{
|
||||||
|
{
|
||||||
|
Var: "$value",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "tagValue",
|
||||||
|
Value: "howdy.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Var: "$tag",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "tagKey",
|
||||||
|
Value: "host",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Var: "$field",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "fieldKey",
|
||||||
|
Value: "field",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `SELECT mean("field") FROM "cpu" WHERE "host" = 'howdy.com' GROUP BY "host"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existant parameters",
|
||||||
|
query: `SELECT $field FROM "cpu"`,
|
||||||
|
want: `SELECT $field FROM "cpu"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "var without a value",
|
||||||
|
query: `SELECT $field FROM "cpu"`,
|
||||||
|
vars: []chronograf.TemplateVar{
|
||||||
|
{
|
||||||
|
Var: "$field",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `SELECT $field FROM "cpu"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "var with unknown type",
|
||||||
|
query: `SELECT $field FROM "cpu"`,
|
||||||
|
vars: []chronograf.TemplateVar{
|
||||||
|
{
|
||||||
|
Var: "$field",
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "who knows?",
|
||||||
|
Value: "field",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: `SELECT $field FROM "cpu"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := TemplateReplace(tt.query, tt.vars)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("TestParse %s =\n%s\nwant\n%s", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,261 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bouk/httprouter"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultWidth is used if not specified
|
||||||
|
DefaultWidth = 4
|
||||||
|
// DefaultHeight is used if not specified
|
||||||
|
DefaultHeight = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type dashboardCellLinks struct {
|
||||||
|
Self string `json:"self"` // Self link mapping to this resource
|
||||||
|
}
|
||||||
|
|
||||||
|
type dashboardCellResponse struct {
|
||||||
|
chronograf.DashboardCell
|
||||||
|
Links dashboardCellLinks `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse {
|
||||||
|
base := "/chronograf/v1/dashboards"
|
||||||
|
cells := make([]dashboardCellResponse, len(dcells))
|
||||||
|
for i, cell := range dcells {
|
||||||
|
if len(cell.Queries) == 0 {
|
||||||
|
cell.Queries = make([]chronograf.DashboardQuery, 0)
|
||||||
|
}
|
||||||
|
cells[i] = dashboardCellResponse{
|
||||||
|
DashboardCell: cell,
|
||||||
|
Links: dashboardCellLinks{
|
||||||
|
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidDashboardCellRequest verifies that the dashboard cells have a query
|
||||||
|
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
||||||
|
CorrectWidthHeight(c)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CorrectWidthHeight changes the cell to have at least the
|
||||||
|
// minimum width and height
|
||||||
|
func CorrectWidthHeight(c *chronograf.DashboardCell) {
|
||||||
|
if c.W < 1 {
|
||||||
|
c.W = DefaultWidth
|
||||||
|
}
|
||||||
|
if c.H < 1 {
|
||||||
|
c.H = DefaultHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := ToQueryConfig(q.Command)
|
||||||
|
q.QueryConfig = qc
|
||||||
|
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 adding cell %s to dashboard %d: %v", cid, 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 gets a specific cell from 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 removes a specific cell from 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 removing cell %s from dashboard %d: %v", cid, id, err)
|
||||||
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceDashboardCell replaces a cell entirely within 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 cell %s in dashboard %d: %v", cid, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,37 +5,21 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/bouk/httprouter"
|
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
"github.com/influxdata/chronograf/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultWidth is used if not specified
|
|
||||||
DefaultWidth = 4
|
|
||||||
// DefaultHeight is used if not specified
|
|
||||||
DefaultHeight = 4
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type dashboardLinks struct {
|
type dashboardLinks struct {
|
||||||
Self string `json:"self"` // Self link mapping to this resource
|
Self string `json:"self"` // Self link mapping to this resource
|
||||||
Cells string `json:"cells"` // Cells link to the cells endpoint
|
Cells string `json:"cells"` // Cells link to the cells endpoint
|
||||||
}
|
Templates string `json:"templates"` // Templates link to the templates 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 {
|
type dashboardResponse struct {
|
||||||
ID chronograf.DashboardID `json:"id"`
|
ID chronograf.DashboardID `json:"id"`
|
||||||
Cells []dashboardCellResponse `json:"cells"`
|
Cells []dashboardCellResponse `json:"cells"`
|
||||||
Name string `json:"name"`
|
Templates []templateResponse `json:"templates"`
|
||||||
Links dashboardLinks `json:"links"`
|
Name string `json:"name"`
|
||||||
|
Links dashboardLinks `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type getDashboardsResponse struct {
|
type getDashboardsResponse struct {
|
||||||
|
@ -46,25 +30,18 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
|
||||||
base := "/chronograf/v1/dashboards"
|
base := "/chronograf/v1/dashboards"
|
||||||
DashboardDefaults(&d)
|
DashboardDefaults(&d)
|
||||||
AddQueryConfigs(&d)
|
AddQueryConfigs(&d)
|
||||||
cells := make([]dashboardCellResponse, len(d.Cells))
|
cells := newCellResponses(d.ID, d.Cells)
|
||||||
for i, cell := range d.Cells {
|
templates := newTemplateResponses(d.ID, d.Templates)
|
||||||
if len(cell.Queries) == 0 {
|
|
||||||
cell.Queries = make([]chronograf.DashboardQuery, 0)
|
|
||||||
}
|
|
||||||
cells[i] = dashboardCellResponse{
|
|
||||||
DashboardCell: cell,
|
|
||||||
Links: dashboardCellLinks{
|
|
||||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, d.ID, cell.ID),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &dashboardResponse{
|
return &dashboardResponse{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Cells: cells,
|
Cells: cells,
|
||||||
|
Templates: templates,
|
||||||
Links: dashboardLinks{
|
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),
|
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID),
|
||||||
|
Templates: fmt.Sprintf("%s/%d/templates", base, d.ID),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +62,6 @@ func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, dashboard := range dashboards {
|
for _, dashboard := range dashboards {
|
||||||
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard))
|
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard))
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,19 +219,20 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
// ValidDashboardRequest verifies that the dashboard cells have a query
|
// ValidDashboardRequest verifies that the dashboard cells have a query
|
||||||
func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
||||||
for i, c := range d.Cells {
|
for i, c := range d.Cells {
|
||||||
CorrectWidthHeight(&c)
|
if err := ValidDashboardCellRequest(&c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
d.Cells[i] = c
|
d.Cells[i] = c
|
||||||
}
|
}
|
||||||
|
for _, t := range d.Templates {
|
||||||
|
if err := ValidTemplateRequest(&t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
DashboardDefaults(d)
|
DashboardDefaults(d)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidDashboardCellRequest verifies that the dashboard cells have a query
|
|
||||||
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
|
||||||
CorrectWidthHeight(c)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DashboardDefaults updates the dashboard with the default values
|
// DashboardDefaults updates the dashboard with the default values
|
||||||
// if none are specified
|
// if none are specified
|
||||||
func DashboardDefaults(d *chronograf.Dashboard) {
|
func DashboardDefaults(d *chronograf.Dashboard) {
|
||||||
|
@ -265,17 +242,6 @@ func DashboardDefaults(d *chronograf.Dashboard) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CorrectWidthHeight changes the cell to have at least the
|
|
||||||
// minimum width and height
|
|
||||||
func CorrectWidthHeight(c *chronograf.DashboardCell) {
|
|
||||||
if c.W < 1 {
|
|
||||||
c.W = DefaultWidth
|
|
||||||
}
|
|
||||||
if c.H < 1 {
|
|
||||||
c.H = DefaultHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddQueryConfigs updates all the celsl in the dashboard to have query config
|
// AddQueryConfigs updates all the celsl in the dashboard to have query config
|
||||||
// objects corresponding to their influxql queries.
|
// objects corresponding to their influxql queries.
|
||||||
func AddQueryConfigs(d *chronograf.Dashboard) {
|
func AddQueryConfigs(d *chronograf.Dashboard) {
|
||||||
|
@ -284,203 +250,3 @@ func AddQueryConfigs(d *chronograf.Dashboard) {
|
||||||
d.Cells[i] = 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 := ToQueryConfig(q.Command)
|
|
||||||
q.QueryConfig = qc
|
|
||||||
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 adding cell %s to dashboard %d: %v", cid, 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 removing cell %s from dashboard %d: %v", cid, 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 cell %s in dashboard %d: %v", cid, 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -233,6 +233,7 @@ func Test_newDashboardResponse(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &dashboardResponse{
|
want: &dashboardResponse{
|
||||||
|
Templates: []templateResponse{},
|
||||||
Cells: []dashboardCellResponse{
|
Cells: []dashboardCellResponse{
|
||||||
dashboardCellResponse{
|
dashboardCellResponse{
|
||||||
Links: dashboardCellLinks{
|
Links: dashboardCellLinks{
|
||||||
|
@ -289,8 +290,9 @@ func Test_newDashboardResponse(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Links: dashboardLinks{
|
Links: dashboardLinks{
|
||||||
Self: "/chronograf/v1/dashboards/0",
|
Self: "/chronograf/v1/dashboards/0",
|
||||||
Cells: "/chronograf/v1/dashboards/0/cells",
|
Cells: "/chronograf/v1/dashboards/0/cells",
|
||||||
|
Templates: "/chronograf/v1/dashboards/0/templates",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -155,6 +155,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
|
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
|
||||||
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
||||||
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
||||||
|
// Dashboard Templates
|
||||||
|
router.GET("/chronograf/v1/dashboards/:id/templates", service.Templates)
|
||||||
|
router.POST("/chronograf/v1/dashboards/:id/templates", service.NewTemplate)
|
||||||
|
|
||||||
|
router.GET("/chronograf/v1/dashboards/:id/templates/:tid", service.TemplateID)
|
||||||
|
router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", service.RemoveTemplate)
|
||||||
|
router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", service.ReplaceTemplate)
|
||||||
|
|
||||||
// Databases
|
// Databases
|
||||||
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)
|
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)
|
||||||
|
|
|
@ -8,12 +8,14 @@ import (
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/influx"
|
||||||
"github.com/influxdata/chronograf/influx/queries"
|
"github.com/influxdata/chronograf/influx/queries"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueryRequest struct {
|
type QueryRequest struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
|
TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueriesRequest struct {
|
type QueriesRequest struct {
|
||||||
|
@ -21,10 +23,12 @@ type QueriesRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryResponse struct {
|
type QueryResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
QueryConfig chronograf.QueryConfig `json:"queryConfig"`
|
QueryConfig chronograf.QueryConfig `json:"queryConfig"`
|
||||||
QueryAST *queries.SelectStatement `json:"queryAST,omitempty"`
|
QueryAST *queries.SelectStatement `json:"queryAST,omitempty"`
|
||||||
|
QueryTemplated *string `json:"queryTemplated,omitempty"`
|
||||||
|
TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueriesResponse struct {
|
type QueriesResponse struct {
|
||||||
|
@ -62,17 +66,28 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
|
||||||
Query: q.Query,
|
Query: q.Query,
|
||||||
}
|
}
|
||||||
|
|
||||||
qc := ToQueryConfig(q.Query)
|
query := q.Query
|
||||||
|
if len(q.TemplateVars) > 0 {
|
||||||
|
query = influx.TemplateReplace(query, q.TemplateVars)
|
||||||
|
qr.QueryTemplated = &query
|
||||||
|
}
|
||||||
|
|
||||||
|
qc := ToQueryConfig(query)
|
||||||
if err := s.DefaultRP(ctx, &qc, &src); err != nil {
|
if err := s.DefaultRP(ctx, &qc, &src); err != nil {
|
||||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
qr.QueryConfig = qc
|
qr.QueryConfig = qc
|
||||||
|
|
||||||
if stmt, err := queries.ParseSelect(q.Query); err == nil {
|
if stmt, err := queries.ParseSelect(query); err == nil {
|
||||||
qr.QueryAST = stmt
|
qr.QueryAST = stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(q.TemplateVars) > 0 {
|
||||||
|
qr.TemplateVars = q.TemplateVars
|
||||||
|
qr.QueryConfig.RawText = &qr.Query
|
||||||
|
}
|
||||||
|
|
||||||
qr.QueryConfig.ID = q.ID
|
qr.QueryConfig.ID = q.ID
|
||||||
res.Queries[i] = qr
|
res.Queries[i] = qr
|
||||||
}
|
}
|
||||||
|
|
|
@ -3055,10 +3055,20 @@
|
||||||
"Proxy": {
|
"Proxy": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"example": {
|
"example": {
|
||||||
"query": "select * from cpu where time > now() - 10m",
|
"query": "select $myfield from cpu where time > now() - 10m",
|
||||||
"db": "telegraf",
|
"db": "telegraf",
|
||||||
"rp": "autogen",
|
"rp": "autogen",
|
||||||
"format": "raw"
|
"tempVars": [
|
||||||
|
{
|
||||||
|
"tempVar": "$myfield",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"type": "fieldKey",
|
||||||
|
"value": "usage_user"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"query"
|
"query"
|
||||||
|
@ -3073,12 +3083,49 @@
|
||||||
"rp": {
|
"rp": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"format": {
|
"tempVars": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Template variables to replace within an InfluxQL query",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/TemplateVariable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TemplateVariable": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Named variable within an InfluxQL query to be replaced with values",
|
||||||
|
"properties": {
|
||||||
|
"tempVar": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "String to replace within an InfluxQL statement"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Values used to replace tempVar.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/TemplateValue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TemplateValue": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Value use to replace a template in an InfluxQL query. The type governs the output format",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific value that will be encoded based on type"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"raw"
|
"csv",
|
||||||
|
"tagKey",
|
||||||
|
"tagValue",
|
||||||
|
"fieldKey"
|
||||||
],
|
],
|
||||||
"default": "raw"
|
"description": "The type will change the format of the output value. tagKey/fieldKey are double quoted; tagValue are single quoted; csv are not quoted."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3986,4 +4033,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bouk/httprouter"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidTemplateRequest checks if the request sent to the server is the correct format.
|
||||||
|
func ValidTemplateRequest(template *chronograf.Template) error {
|
||||||
|
switch template.Type {
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown template type %s", template.Type)
|
||||||
|
case "query", "constant", "csv", "fieldKeys", "tagKeys", "tagValues", "measurements", "databases":
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range template.Values {
|
||||||
|
switch v.Type {
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown template variable type %s", v.Type)
|
||||||
|
case "csv", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.Type == "query" && template.Query == nil {
|
||||||
|
return fmt.Errorf("No query set for template of type 'query'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type templateLinks struct {
|
||||||
|
Self string `json:"self"` // Self link mapping to this resource
|
||||||
|
}
|
||||||
|
|
||||||
|
type templateResponse struct {
|
||||||
|
chronograf.Template
|
||||||
|
Links templateLinks `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTemplateResponses(dID chronograf.DashboardID, tmps []chronograf.Template) []templateResponse {
|
||||||
|
res := make([]templateResponse, len(tmps))
|
||||||
|
for i, t := range tmps {
|
||||||
|
res[i] = newTemplateResponse(dID, t)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type templatesResponses struct {
|
||||||
|
Templates []templateResponse `json:"templates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTemplateResponse(dID chronograf.DashboardID, tmp chronograf.Template) templateResponse {
|
||||||
|
base := "/chronograf/v1/dashboards"
|
||||||
|
return templateResponse{
|
||||||
|
Template: tmp,
|
||||||
|
Links: templateLinks{
|
||||||
|
Self: fmt.Sprintf("%s/%d/templates/%s", base, dID, tmp.ID),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates returns all templates from a dashboard within the store
|
||||||
|
func (s *Service) Templates(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()
|
||||||
|
d, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
||||||
|
if err != nil {
|
||||||
|
notFound(w, id, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := templatesResponses{
|
||||||
|
Templates: newTemplateResponses(chronograf.DashboardID(id), d.Templates),
|
||||||
|
}
|
||||||
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplate adds a template to an existing dashboard
|
||||||
|
func (s *Service) NewTemplate(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 template chronograf.Template
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
|
||||||
|
invalidJSON(w, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidTemplateRequest(&template); err != nil {
|
||||||
|
invalidData(w, err, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := uuid.V4{}
|
||||||
|
tid, err := ids.Generate()
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Error creating template ID for dashboard %d: %v", id, err)
|
||||||
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template.ID = chronograf.TemplateID(tid)
|
||||||
|
|
||||||
|
dash.Templates = append(dash.Templates, template)
|
||||||
|
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||||
|
msg := fmt.Sprintf("Error adding template %s to dashboard %d: %v", tid, id, err)
|
||||||
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := newTemplateResponse(dash.ID, template)
|
||||||
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateID retrieves a specific template from a dashboard
|
||||||
|
func (s *Service) TemplateID(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
|
||||||
|
}
|
||||||
|
|
||||||
|
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||||
|
for _, t := range dash.Templates {
|
||||||
|
if t.ID == chronograf.TemplateID(tid) {
|
||||||
|
res := newTemplateResponse(chronograf.DashboardID(id), t)
|
||||||
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notFound(w, id, s.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTemplate removes a specific template from an existing dashboard
|
||||||
|
func (s *Service) RemoveTemplate(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
|
||||||
|
}
|
||||||
|
|
||||||
|
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||||
|
pos := -1
|
||||||
|
for i, t := range dash.Templates {
|
||||||
|
if t.ID == chronograf.TemplateID(tid) {
|
||||||
|
pos = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pos == -1 {
|
||||||
|
notFound(w, id, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dash.Templates = append(dash.Templates[:pos], dash.Templates[pos+1:]...)
|
||||||
|
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||||
|
msg := fmt.Sprintf("Error removing template %s from dashboard %d: %v", tid, id, err)
|
||||||
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceTemplate replaces a template entirely within an existing dashboard
|
||||||
|
func (s *Service) ReplaceTemplate(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
|
||||||
|
}
|
||||||
|
|
||||||
|
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||||
|
pos := -1
|
||||||
|
for i, t := range dash.Templates {
|
||||||
|
if t.ID == chronograf.TemplateID(tid) {
|
||||||
|
pos = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pos == -1 {
|
||||||
|
notFound(w, id, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var template chronograf.Template
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
|
||||||
|
invalidJSON(w, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidTemplateRequest(&template); err != nil {
|
||||||
|
invalidData(w, err, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template.ID = chronograf.TemplateID(tid)
|
||||||
|
|
||||||
|
dash.Templates[pos] = template
|
||||||
|
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||||
|
msg := fmt.Sprintf("Error updating template %s in dashboard %d: %v", tid, id, err)
|
||||||
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := newTemplateResponse(chronograf.DashboardID(id), template)
|
||||||
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidTemplateRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
template *chronograf.Template
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Template",
|
||||||
|
template: &chronograf.Template{
|
||||||
|
Type: "fieldKeys",
|
||||||
|
TemplateVar: chronograf.TemplateVar{
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "fieldKey",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Template Type",
|
||||||
|
wantErr: true,
|
||||||
|
template: &chronograf.Template{
|
||||||
|
Type: "Unknown Type",
|
||||||
|
TemplateVar: chronograf.TemplateVar{
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "fieldKey",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Template Variable Type",
|
||||||
|
wantErr: true,
|
||||||
|
template: &chronograf.Template{
|
||||||
|
Type: "csv",
|
||||||
|
TemplateVar: chronograf.TemplateVar{
|
||||||
|
Values: []chronograf.TemplateValue{
|
||||||
|
{
|
||||||
|
Type: "unknown value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No query set",
|
||||||
|
wantErr: true,
|
||||||
|
template: &chronograf.Template{
|
||||||
|
Type: "query",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := ValidTemplateRequest(tt.template); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ValidTemplateRequest() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -149,7 +149,7 @@
|
||||||
'eol-last': 0, // TODO: revisit
|
'eol-last': 0, // TODO: revisit
|
||||||
'id-length': 0,
|
'id-length': 0,
|
||||||
'id-match': 0,
|
'id-match': 0,
|
||||||
'indent': [2, 2, {SwitchCase: 1}],
|
'indent': [0, 2, {SwitchCase: 1}],
|
||||||
'key-spacing': [2, {beforeColon: false, afterColon: true}],
|
'key-spacing': [2, {beforeColon: false, afterColon: true}],
|
||||||
'linebreak-style': [2, 'unix'],
|
'linebreak-style': [2, 'unix'],
|
||||||
'lines-around-comment': 0,
|
'lines-around-comment': 0,
|
||||||
|
@ -234,6 +234,6 @@
|
||||||
'react/require-extension': 0,
|
'react/require-extension': 0,
|
||||||
'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons)
|
'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons)
|
||||||
'react/sort-comp': 0, // TODO: 2
|
'react/sort-comp': 0, // TODO: 2
|
||||||
'react/jsx-wrap-multilines': 'error',
|
'react/jsx-wrap-multilines': ['error', {'declaration': false, 'assignment': false}],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
"mocha-loader": "^0.7.1",
|
"mocha-loader": "^0.7.1",
|
||||||
"mustache": "^2.2.1",
|
"mustache": "^2.2.1",
|
||||||
"node-sass": "^3.5.3",
|
"node-sass": "^4.5.2",
|
||||||
"postcss-browser-reporter": "^0.4.0",
|
"postcss-browser-reporter": "^0.4.0",
|
||||||
"postcss-calc": "^5.2.0",
|
"postcss-calc": "^5.2.0",
|
||||||
"postcss-loader": "^0.8.0",
|
"postcss-loader": "^0.8.0",
|
||||||
|
|
|
@ -10,11 +10,48 @@ import {
|
||||||
editDashboardCell,
|
editDashboardCell,
|
||||||
renameDashboardCell,
|
renameDashboardCell,
|
||||||
syncDashboardCell,
|
syncDashboardCell,
|
||||||
|
templateVariableSelected,
|
||||||
} from 'src/dashboards/actions'
|
} from 'src/dashboards/actions'
|
||||||
|
|
||||||
let state
|
let state
|
||||||
const d1 = {id: 1, cells: [], name: "d1"}
|
const templates = [
|
||||||
const d2 = {id: 2, cells: [], name: "d2"}
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'query',
|
||||||
|
label: 'test query',
|
||||||
|
tempVar: '$REGION',
|
||||||
|
query: {
|
||||||
|
db: 'db1',
|
||||||
|
rp: 'rp1',
|
||||||
|
measurement: 'm1',
|
||||||
|
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
{value: 'us-west', type: 'tagKey', selected: false},
|
||||||
|
{value: 'us-east', type: 'tagKey', selected: true},
|
||||||
|
{value: 'us-mount', type: 'tagKey', selected: false},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'csv',
|
||||||
|
label: 'test csv',
|
||||||
|
tempVar: '$TEMPERATURE',
|
||||||
|
values: [
|
||||||
|
{value: '98.7', type: 'measurement', selected: false},
|
||||||
|
{value: '99.1', type: 'measurement', selected: false},
|
||||||
|
{value: '101.3', type: 'measurement', selected: true},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const d1 = {
|
||||||
|
id: 1,
|
||||||
|
cells: [],
|
||||||
|
name: 'd1',
|
||||||
|
templates,
|
||||||
|
}
|
||||||
|
const d2 = {id: 2, cells: [], name: 'd2', templates: []}
|
||||||
const dashboards = [d1, d2]
|
const dashboards = [d1, d2]
|
||||||
const c1 = {
|
const c1 = {
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -23,9 +60,21 @@ const c1 = {
|
||||||
h: 4,
|
h: 4,
|
||||||
id: 1,
|
id: 1,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
name: "Gigawatts",
|
name: 'Gigawatts',
|
||||||
}
|
}
|
||||||
const cells = [c1]
|
const cells = [c1]
|
||||||
|
const tempVar = {
|
||||||
|
...d1.templates[0],
|
||||||
|
id: '1',
|
||||||
|
type: 'measurement',
|
||||||
|
label: 'test query',
|
||||||
|
tempVar: '$HOSTS',
|
||||||
|
query: {
|
||||||
|
db: 'db1',
|
||||||
|
text: 'SHOW TAGS WHERE HUNTER = "coo"',
|
||||||
|
},
|
||||||
|
values: ['h1', 'h2', 'h3'],
|
||||||
|
}
|
||||||
|
|
||||||
describe('DataExplorer.Reducers.UI', () => {
|
describe('DataExplorer.Reducers.UI', () => {
|
||||||
it('can load the dashboards', () => {
|
it('can load the dashboards', () => {
|
||||||
|
@ -66,6 +115,7 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
id: 1,
|
id: 1,
|
||||||
cells: updatedCells,
|
cells: updatedCells,
|
||||||
name: 'd1',
|
name: 'd1',
|
||||||
|
templates,
|
||||||
}
|
}
|
||||||
|
|
||||||
const actual = reducer(state, updateDashboardCells(d1, updatedCells))
|
const actual = reducer(state, updateDashboardCells(d1, updatedCells))
|
||||||
|
@ -106,7 +156,28 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
dashboards: [dash],
|
dashboards: [dash],
|
||||||
}
|
}
|
||||||
|
|
||||||
const actual = reducer(state, renameDashboardCell(dash, 0, 0, "Plutonium Consumption Rate (ug/sec)"))
|
const actual = reducer(
|
||||||
expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
|
state,
|
||||||
|
renameDashboardCell(dash, 0, 0, 'Plutonium Consumption Rate (ug/sec)')
|
||||||
|
)
|
||||||
|
expect(actual.dashboards[0].cells[0].name).to.equal(
|
||||||
|
'Plutonium Consumption Rate (ug/sec)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can select a different template variable', () => {
|
||||||
|
const dash = _.cloneDeep(d1)
|
||||||
|
state = {
|
||||||
|
dashboards: [dash],
|
||||||
|
}
|
||||||
|
const value = dash.templates[0].values[2].value
|
||||||
|
const actual = reducer(
|
||||||
|
{dashboards},
|
||||||
|
templateVariableSelected(dash.id, dash.templates[0].id, [{value}])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(actual.dashboards[0].templates[0].values[0].selected).to.equal(false)
|
||||||
|
expect(actual.dashboards[0].templates[0].values[1].selected).to.equal(false)
|
||||||
|
expect(actual.dashboards[0].templates[0].values[2].selected).to.equal(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import {TEMPLATE_MATCHER} from 'src/dashboards/constants'
|
||||||
|
|
||||||
|
describe('templating', () => {
|
||||||
|
describe('matching', () => {
|
||||||
|
it('can match the expected strings', () => {
|
||||||
|
const matchingStrings = [
|
||||||
|
'SELECT : FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
'SELECT :t, "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
'SELECT :tv1, "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
'SELECT "f1" FROM "db1"."rp1"."m1" WHERE time > now() - :tv',
|
||||||
|
]
|
||||||
|
|
||||||
|
matchingStrings.forEach(s => {
|
||||||
|
const result = s.match(TEMPLATE_MATCHER)
|
||||||
|
expect(result.length).to.be.above(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not match unexpected strings', () => {
|
||||||
|
const nonMatchingStrings = [
|
||||||
|
'SELECT "foo", "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
'SELECT :tv1:, :tv2: FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
]
|
||||||
|
|
||||||
|
nonMatchingStrings.forEach(s => {
|
||||||
|
const result = s.match(TEMPLATE_MATCHER)
|
||||||
|
expect(result).to.equal(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only matches when starts with : but does not end in :', () => {
|
||||||
|
const matchingStrings = [
|
||||||
|
'SELECT :tv1, :tv2: FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
'SELECT :tv1:, :tv2 FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||||
|
]
|
||||||
|
|
||||||
|
matchingStrings.forEach(s => {
|
||||||
|
const result = s.match(TEMPLATE_MATCHER)
|
||||||
|
expect(result.length).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('replacing', () => {
|
||||||
|
const tempVar = ':tv1:'
|
||||||
|
it('can replace the expected strings', () => {
|
||||||
|
const s = 'SELECT :fasdf FROM "db1"."rp1"."m1"'
|
||||||
|
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||||
|
const expected = `SELECT ${tempVar} FROM "db1"."rp1"."m1"`
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can replace a string with a numeric character', () => {
|
||||||
|
const s = 'SELECT :fas0df FROM "db1"."rp1"."m1"'
|
||||||
|
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||||
|
const expected = `SELECT ${tempVar} FROM "db1"."rp1"."m1"`
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can replace the expected strings that are next to ,', () => {
|
||||||
|
const s = 'SELECT :fasdf, "f1" FROM "db1"."rp1"."m1"'
|
||||||
|
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||||
|
const expected = `SELECT ${tempVar}, "f1" FROM "db1"."rp1"."m1"`
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can replace the expected strings that are next to .', () => {
|
||||||
|
const s = 'SELECT "f1" FROM "db1".:asdf."m1"'
|
||||||
|
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||||
|
const expected = `SELECT "f1" FROM "db1".${tempVar}."m1"`
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can does not replace other tempVars', () => {
|
||||||
|
const s = 'SELECT :foo: FROM "db1".:asdfasd."m1"'
|
||||||
|
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||||
|
const expected = `SELECT :foo: FROM "db1".${tempVar}."m1"`
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -12,21 +12,53 @@ import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||||
// Acts as a 'router middleware'. The main `App` component is responsible for
|
// Acts as a 'router middleware'. The main `App` component is responsible for
|
||||||
// getting the list of data nodes, but not every page requires them to function.
|
// getting the list of data nodes, but not every page requires them to function.
|
||||||
// Routes that do require data nodes can be nested under this component.
|
// Routes that do require data nodes can be nested under this component.
|
||||||
|
const {arrayOf, func, node, shape, string} = PropTypes
|
||||||
const CheckSources = React.createClass({
|
const CheckSources = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: PropTypes.node,
|
sources: arrayOf(
|
||||||
params: PropTypes.shape({
|
shape({
|
||||||
sourceID: PropTypes.string,
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
self: string.isRequired,
|
||||||
|
kapacitors: string.isRequired,
|
||||||
|
queries: string.isRequired,
|
||||||
|
permissions: string.isRequired,
|
||||||
|
users: string.isRequired,
|
||||||
|
databases: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
children: node,
|
||||||
|
params: shape({
|
||||||
|
sourceID: string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
router: PropTypes.shape({
|
router: shape({
|
||||||
push: PropTypes.func.isRequired,
|
push: func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
location: PropTypes.shape({
|
location: shape({
|
||||||
pathname: PropTypes.string.isRequired,
|
pathname: string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
sources: PropTypes.array.isRequired,
|
loadSources: func.isRequired,
|
||||||
errorThrown: PropTypes.func.isRequired,
|
errorThrown: func.isRequired,
|
||||||
loadSources: PropTypes.func.isRequired,
|
},
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
self: string.isRequired,
|
||||||
|
kapacitors: string.isRequired,
|
||||||
|
queries: string.isRequired,
|
||||||
|
permissions: string.isRequired,
|
||||||
|
users: string.isRequired,
|
||||||
|
databases: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
const {sources, params: {sourceID}} = this.props
|
||||||
|
return {source: sources.find(s => s.id === sourceID)}
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
@ -51,8 +83,8 @@ const CheckSources = React.createClass({
|
||||||
async componentWillUpdate(nextProps, nextState) {
|
async componentWillUpdate(nextProps, nextState) {
|
||||||
const {router, location, params, errorThrown, sources} = nextProps
|
const {router, location, params, errorThrown, sources} = nextProps
|
||||||
const {isFetching} = nextState
|
const {isFetching} = nextState
|
||||||
const source = sources.find((s) => s.id === params.sourceID)
|
const source = sources.find(s => s.id === params.sourceID)
|
||||||
const defaultSource = sources.find((s) => s.default === true)
|
const defaultSource = sources.find(s => s.default === true)
|
||||||
|
|
||||||
if (!isFetching && !source) {
|
if (!isFetching && !source) {
|
||||||
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
||||||
|
@ -80,15 +112,21 @@ const CheckSources = React.createClass({
|
||||||
render() {
|
render() {
|
||||||
const {params, sources} = this.props
|
const {params, sources} = this.props
|
||||||
const {isFetching} = this.state
|
const {isFetching} = this.state
|
||||||
const source = sources.find((s) => s.id === params.sourceID)
|
const source = sources.find(s => s.id === params.sourceID)
|
||||||
|
|
||||||
if (isFetching || !source) {
|
if (isFetching || !source) {
|
||||||
return <div className="page-spinner" />
|
return <div className="page-spinner" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, {
|
return (
|
||||||
source,
|
this.props.children &&
|
||||||
}))
|
React.cloneElement(
|
||||||
|
this.props.children,
|
||||||
|
Object.assign({}, this.props, {
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -96,9 +134,11 @@ const mapStateToProps = ({sources}) => ({
|
||||||
sources,
|
sources,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
loadSources: bindActionCreators(loadSourcesAction, dispatch),
|
loadSources: bindActionCreators(loadSourcesAction, dispatch),
|
||||||
errorThrown: bindActionCreators(errorThrownAction, dispatch),
|
errorThrown: bindActionCreators(errorThrownAction, dispatch),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CheckSources))
|
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||||
|
withRouter(CheckSources)
|
||||||
|
)
|
||||||
|
|
|
@ -157,7 +157,7 @@ class AdminPage extends Component {
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>
|
<h1 className="page-header__title">
|
||||||
Admin
|
Admin
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -125,7 +125,7 @@ class AlertsApp extends Component {
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>
|
<h1 className="page-header__title">
|
||||||
Alert History
|
Alert History
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {errorThrown} from 'shared/actions/errors'
|
||||||
|
|
||||||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||||
|
|
||||||
|
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||||
|
|
||||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||||
type: 'LOAD_DASHBOARDS',
|
type: 'LOAD_DASHBOARDS',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -20,28 +22,28 @@ export const loadDashboards = (dashboards, dashboardID) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setTimeRange = (timeRange) => ({
|
export const setTimeRange = timeRange => ({
|
||||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||||
payload: {
|
payload: {
|
||||||
timeRange,
|
timeRange,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateDashboard = (dashboard) => ({
|
export const updateDashboard = dashboard => ({
|
||||||
type: 'UPDATE_DASHBOARD',
|
type: 'UPDATE_DASHBOARD',
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteDashboard = (dashboard) => ({
|
export const deleteDashboard = dashboard => ({
|
||||||
type: 'DELETE_DASHBOARD',
|
type: 'DELETE_DASHBOARD',
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteDashboardFailed = (dashboard) => ({
|
export const deleteDashboardFailed = dashboard => ({
|
||||||
type: 'DELETE_DASHBOARD_FAILED',
|
type: 'DELETE_DASHBOARD_FAILED',
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
|
@ -80,8 +82,8 @@ export const editDashboardCell = (dashboard, x, y, isEditing) => ({
|
||||||
// as a suitable id
|
// as a suitable id
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
x, // x-coord of the cell to be edited
|
x, // x-coord of the cell to be edited
|
||||||
y, // y-coord of the cell to be edited
|
y, // y-coord of the cell to be edited
|
||||||
isEditing,
|
isEditing,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -90,15 +92,16 @@ export const renameDashboardCell = (dashboard, x, y, name) => ({
|
||||||
type: 'RENAME_DASHBOARD_CELL',
|
type: 'RENAME_DASHBOARD_CELL',
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
x, // x-coord of the cell to be renamed
|
x, // x-coord of the cell to be renamed
|
||||||
y, // y-coord of the cell to be renamed
|
y, // y-coord of the cell to be renamed
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteDashboardCell = (cell) => ({
|
export const deleteDashboardCell = (dashboard, cell) => ({
|
||||||
type: 'DELETE_DASHBOARD_CELL',
|
type: 'DELETE_DASHBOARD_CELL',
|
||||||
payload: {
|
payload: {
|
||||||
|
dashboard,
|
||||||
cell,
|
cell,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -111,65 +114,84 @@ export const editCellQueryStatus = (queryID, status) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const templateVariableSelected = (dashboardID, templateID, values) => ({
|
||||||
|
type: TEMPLATE_VARIABLE_SELECTED,
|
||||||
|
payload: {
|
||||||
|
dashboardID,
|
||||||
|
templateID,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Async Action Creators
|
// Async Action Creators
|
||||||
|
|
||||||
export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
|
export const getDashboardsAsync = () => async dispatch => {
|
||||||
try {
|
try {
|
||||||
const {data: {dashboards}} = await getDashboardsAJAX()
|
const {data: {dashboards}} = await getDashboardsAJAX()
|
||||||
dispatch(loadDashboards(dashboards, dashboardID))
|
dispatch(loadDashboards(dashboards))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(errorThrown(error))
|
|
||||||
console.error(error)
|
console.error(error)
|
||||||
throw error
|
dispatch(errorThrown(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const putDashboard = (dashboard) => async (dispatch) => {
|
export const putDashboard = dashboard => async dispatch => {
|
||||||
try {
|
try {
|
||||||
const {data} = await updateDashboardAJAX(dashboard)
|
const {data} = await updateDashboardAJAX(dashboard)
|
||||||
dispatch(updateDashboard(data))
|
dispatch(updateDashboard(data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
dispatch(errorThrown(error))
|
dispatch(errorThrown(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateDashboardCell = (dashboard, cell) => async (dispatch) => {
|
export const updateDashboardCell = (dashboard, cell) => async dispatch => {
|
||||||
try {
|
try {
|
||||||
const {data} = await updateDashboardCellAJAX(cell)
|
const {data} = await updateDashboardCellAJAX(cell)
|
||||||
dispatch(syncDashboardCell(dashboard, data))
|
dispatch(syncDashboardCell(dashboard, data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
dispatch(errorThrown(error))
|
dispatch(errorThrown(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteDashboardAsync = (dashboard) => async (dispatch) => {
|
export const deleteDashboardAsync = dashboard => async dispatch => {
|
||||||
dispatch(deleteDashboard(dashboard))
|
dispatch(deleteDashboard(dashboard))
|
||||||
try {
|
try {
|
||||||
await deleteDashboardAJAX(dashboard)
|
await deleteDashboardAJAX(dashboard)
|
||||||
dispatch(publishAutoDismissingNotification('success', 'Dashboard deleted successfully.'))
|
dispatch(
|
||||||
|
publishAutoDismissingNotification(
|
||||||
|
'success',
|
||||||
|
'Dashboard deleted successfully.'
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`))
|
dispatch(
|
||||||
|
errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`)
|
||||||
|
)
|
||||||
dispatch(deleteDashboardFailed(dashboard))
|
dispatch(deleteDashboardFailed(dashboard))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addDashboardCellAsync = (dashboard) => async (dispatch) => {
|
export const addDashboardCellAsync = dashboard => async dispatch => {
|
||||||
try {
|
try {
|
||||||
const {data} = await addDashboardCellAJAX(dashboard, NEW_DEFAULT_DASHBOARD_CELL)
|
const {data} = await addDashboardCellAJAX(
|
||||||
|
dashboard,
|
||||||
|
NEW_DEFAULT_DASHBOARD_CELL
|
||||||
|
)
|
||||||
dispatch(addDashboardCell(dashboard, data))
|
dispatch(addDashboardCell(dashboard, data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(errorThrown(error))
|
|
||||||
console.error(error)
|
console.error(error)
|
||||||
throw error
|
dispatch(errorThrown(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteDashboardCellAsync = (cell) => async (dispatch) => {
|
export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
||||||
try {
|
try {
|
||||||
await deleteDashboardCellAJAX(cell)
|
await deleteDashboardCellAJAX(cell)
|
||||||
dispatch(deleteDashboardCell(cell))
|
dispatch(deleteDashboardCell(dashboard, cell))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
dispatch(errorThrown(error))
|
dispatch(errorThrown(error))
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AJAX from 'utils/ajax'
|
import AJAX from 'utils/ajax'
|
||||||
|
import {proxy} from 'utils/queryUrlGenerator'
|
||||||
|
|
||||||
export function getDashboards() {
|
export function getDashboards() {
|
||||||
return AJAX({
|
return AJAX({
|
||||||
|
@ -23,7 +24,7 @@ export function updateDashboardCell(cell) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createDashboard = async (dashboard) => {
|
export const createDashboard = async dashboard => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -36,7 +37,7 @@ export const createDashboard = async (dashboard) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteDashboard = async (dashboard) => {
|
export const deleteDashboard = async dashboard => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -61,7 +62,7 @@ export const addDashboardCell = async (dashboard, cell) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteDashboardCell = async (cell) => {
|
export const deleteDashboardCell = async cell => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -72,3 +73,34 @@ export const deleteDashboardCell = async (cell) => {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const editTemplateVariables = async templateVariable => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'PUT',
|
||||||
|
url: templateVariable.links.self,
|
||||||
|
data: templateVariable,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runTemplateVariableQuery = async (
|
||||||
|
source,
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
db,
|
||||||
|
// rp, TODO
|
||||||
|
tempVars,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// TODO: add rp as argument to proxy
|
||||||
|
return await proxy({source: source.links.proxy, query, db, tempVars})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -131,6 +131,7 @@ class CellEditorOverlay extends Component {
|
||||||
const {
|
const {
|
||||||
source,
|
source,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
templates,
|
||||||
timeRange,
|
timeRange,
|
||||||
autoRefresh,
|
autoRefresh,
|
||||||
editQueryStatus,
|
editQueryStatus,
|
||||||
|
@ -174,6 +175,7 @@ class CellEditorOverlay extends Component {
|
||||||
/>
|
/>
|
||||||
<QueryMaker
|
<QueryMaker
|
||||||
source={source}
|
source={source}
|
||||||
|
templates={templates}
|
||||||
queries={queriesWorkingDraft}
|
queries={queriesWorkingDraft}
|
||||||
actions={queryActions}
|
actions={queryActions}
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
|
@ -190,12 +192,17 @@ class CellEditorOverlay extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {func, number, shape, string} = PropTypes
|
const {arrayOf, func, number, shape, string} = PropTypes
|
||||||
|
|
||||||
CellEditorOverlay.propTypes = {
|
CellEditorOverlay.propTypes = {
|
||||||
onCancel: func.isRequired,
|
onCancel: func.isRequired,
|
||||||
onSave: func.isRequired,
|
onSave: func.isRequired,
|
||||||
cell: shape({}).isRequired,
|
cell: shape({}).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
timeRange: shape({
|
timeRange: shape({
|
||||||
upper: string,
|
upper: string,
|
||||||
lower: string,
|
lower: string,
|
||||||
|
@ -203,9 +210,10 @@ CellEditorOverlay.propTypes = {
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
source: shape({
|
source: shape({
|
||||||
links: shape({
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
queries: string.isRequired,
|
queries: string.isRequired,
|
||||||
}),
|
}).isRequired,
|
||||||
}),
|
}).isRequired,
|
||||||
editQueryStatus: func.isRequired,
|
editQueryStatus: func.isRequired,
|
||||||
queryStatus: shape({
|
queryStatus: shape({
|
||||||
queryID: string,
|
queryID: string,
|
||||||
|
|
|
@ -1,31 +1,37 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import omit from 'lodash/omit'
|
||||||
|
|
||||||
import LayoutRenderer from 'shared/components/LayoutRenderer'
|
import LayoutRenderer from 'shared/components/LayoutRenderer'
|
||||||
|
import Dropdown from 'shared/components/Dropdown'
|
||||||
|
|
||||||
const Dashboard = ({
|
const Dashboard = ({
|
||||||
|
source,
|
||||||
|
timeRange,
|
||||||
dashboard,
|
dashboard,
|
||||||
isEditMode,
|
|
||||||
inPresentationMode,
|
|
||||||
onAddCell,
|
onAddCell,
|
||||||
onPositionChange,
|
|
||||||
onEditCell,
|
onEditCell,
|
||||||
|
autoRefresh,
|
||||||
onRenameCell,
|
onRenameCell,
|
||||||
onUpdateCell,
|
onUpdateCell,
|
||||||
onDeleteCell,
|
onDeleteCell,
|
||||||
|
onPositionChange,
|
||||||
|
inPresentationMode,
|
||||||
|
onOpenTemplateManager,
|
||||||
onSummonOverlayTechnologies,
|
onSummonOverlayTechnologies,
|
||||||
source,
|
onSelectTemplate,
|
||||||
autoRefresh,
|
|
||||||
timeRange,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (dashboard.id === 0) {
|
if (dashboard.id === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const cells = dashboard.cells.map((cell) => {
|
const {templates} = dashboard
|
||||||
|
|
||||||
|
const cells = dashboard.cells.map(cell => {
|
||||||
const dashboardCell = {...cell}
|
const dashboardCell = {...cell}
|
||||||
dashboardCell.queries = dashboardCell.queries.map(({label, query, queryConfig, db}) =>
|
dashboardCell.queries = dashboardCell.queries.map(
|
||||||
({
|
({label, query, queryConfig, db}) => ({
|
||||||
label,
|
label,
|
||||||
query,
|
query,
|
||||||
queryConfig,
|
queryConfig,
|
||||||
|
@ -38,11 +44,47 @@ const Dashboard = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames({'page-contents': true, 'presentation-mode': inPresentationMode})}>
|
<div
|
||||||
{cells.length ?
|
className={classnames(
|
||||||
<div className={classnames('container-fluid full-width dashboard', {'dashboard-edit': isEditMode})}>
|
'dashboard container-fluid full-width page-contents',
|
||||||
<LayoutRenderer
|
{'presentation-mode': inPresentationMode}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="template-control-bar">
|
||||||
|
<div className="page-header__left">
|
||||||
|
Template Variables
|
||||||
|
</div>
|
||||||
|
<div className="page-header__right">
|
||||||
|
{templates.map(({id, values}) => {
|
||||||
|
const items = values.map(value => ({...value, text: value.value}))
|
||||||
|
const selectedItem = items.find(item => item.selected) || items[0]
|
||||||
|
const selectedText = selectedItem && selectedItem.text
|
||||||
|
|
||||||
|
// TODO: change Dropdown to a MultiSelectDropdown, `selected` to
|
||||||
|
// the full array, and [item] to all `selected` values when we update
|
||||||
|
// this component to support multiple values
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
key={id}
|
||||||
|
items={items}
|
||||||
|
selected={selectedText || 'Loading...'}
|
||||||
|
onChoose={item =>
|
||||||
|
onSelectTemplate(id, [item].map(x => omit(x, 'text')))}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={onOpenTemplateManager}
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{cells.length
|
||||||
|
? <LayoutRenderer
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
|
templates={templates}
|
||||||
cells={cells}
|
cells={cells}
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
source={source.links.proxy}
|
source={source.links.proxy}
|
||||||
|
@ -53,32 +95,20 @@ const Dashboard = ({
|
||||||
onDeleteCell={onDeleteCell}
|
onDeleteCell={onDeleteCell}
|
||||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||||
/>
|
/>
|
||||||
</div> :
|
: <div className="dashboard__empty">
|
||||||
<div className="dashboard__empty">
|
<p>This Dashboard has no Graphs</p>
|
||||||
<p>This Dashboard has no Graphs</p>
|
<button className="btn btn-primary btn-m" onClick={onAddCell}>
|
||||||
<button
|
Add Graph
|
||||||
className="btn btn-primary btn-m"
|
</button>
|
||||||
onClick={onAddCell}
|
</div>}
|
||||||
>
|
|
||||||
Add Graph
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {arrayOf, bool, func, shape, string, number} = PropTypes
|
||||||
bool,
|
|
||||||
func,
|
|
||||||
shape,
|
|
||||||
string,
|
|
||||||
number,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
Dashboard.propTypes = {
|
Dashboard.propTypes = {
|
||||||
dashboard: shape({}).isRequired,
|
dashboard: shape({}).isRequired,
|
||||||
isEditMode: bool,
|
|
||||||
inPresentationMode: bool,
|
inPresentationMode: bool,
|
||||||
onAddCell: func,
|
onAddCell: func,
|
||||||
onPositionChange: func,
|
onPositionChange: func,
|
||||||
|
@ -94,6 +124,26 @@ Dashboard.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
timeRange: shape({}).isRequired,
|
timeRange: shape({}).isRequired,
|
||||||
|
onOpenTemplateManager: func.isRequired,
|
||||||
|
onSelectTemplate: func.isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
rp: string,
|
||||||
|
influxql: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
value: string.isRequired,
|
||||||
|
selected: bool,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
import Dropdown from 'shared/components/Dropdown'
|
||||||
|
|
||||||
|
import {showDatabases} from 'shared/apis/metaQuery'
|
||||||
|
import parsers from 'shared/parsing'
|
||||||
|
const {databases: showDatabasesParser} = parsers
|
||||||
|
|
||||||
|
class DatabaseDropdown extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
databases: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getDatabases = ::this._getDatabases
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._getDatabases()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {databases} = this.state
|
||||||
|
const {database, onSelectDatabase, onStartEdit} = this.props
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
this.componentDidMount()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
items={databases.map(text => ({text}))}
|
||||||
|
selected={database || 'Loading...'}
|
||||||
|
onChoose={onSelectDatabase}
|
||||||
|
onClick={() => onStartEdit(null)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getDatabases() {
|
||||||
|
const {source} = this.context
|
||||||
|
const {database, onSelectDatabase, onErrorThrown} = this.props
|
||||||
|
const proxy = source.links.proxy
|
||||||
|
try {
|
||||||
|
const {data} = await showDatabases(proxy)
|
||||||
|
const {databases} = showDatabasesParser(data)
|
||||||
|
|
||||||
|
this.setState({databases})
|
||||||
|
const selectedDatabaseText = databases.includes(database)
|
||||||
|
? database
|
||||||
|
: databases[0] || 'No databases'
|
||||||
|
onSelectDatabase({text: selectedDatabaseText})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
onErrorThrown(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
DatabaseDropdown.contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseDropdown.propTypes = {
|
||||||
|
database: string,
|
||||||
|
onSelectDatabase: func.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
onErrorThrown: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseDropdown
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
|
||||||
|
import Dropdown from 'shared/components/Dropdown'
|
||||||
|
import {showMeasurements} from 'shared/apis/metaQuery'
|
||||||
|
import parsers from 'shared/parsing'
|
||||||
|
const {measurements: showMeasurementsParser} = parsers
|
||||||
|
|
||||||
|
class MeasurementDropdown extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
measurements: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getMeasurements = ::this._getMeasurements
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._getMeasurements()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(nextProps) {
|
||||||
|
if (nextProps.database === this.props.database) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getMeasurements()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {measurements} = this.state
|
||||||
|
const {measurement, onSelectMeasurement, onStartEdit} = this.props
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
items={measurements.map(text => ({text}))}
|
||||||
|
selected={measurement || 'Select Measurement'}
|
||||||
|
onChoose={onSelectMeasurement}
|
||||||
|
onClick={() => onStartEdit(null)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMeasurements() {
|
||||||
|
const {source: {links: {proxy}}} = this.context
|
||||||
|
const {
|
||||||
|
measurement,
|
||||||
|
database,
|
||||||
|
onSelectMeasurement,
|
||||||
|
onErrorThrown,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await showMeasurements(proxy, database)
|
||||||
|
const {measurements} = showMeasurementsParser(data)
|
||||||
|
|
||||||
|
this.setState({measurements})
|
||||||
|
const selectedMeasurementText = measurements.includes(measurement)
|
||||||
|
? measurement
|
||||||
|
: measurements[0] || 'No measurements'
|
||||||
|
onSelectMeasurement({text: selectedMeasurementText})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
onErrorThrown(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
MeasurementDropdown.contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
MeasurementDropdown.propTypes = {
|
||||||
|
database: string.isRequired,
|
||||||
|
measurement: string,
|
||||||
|
onSelectMeasurement: func.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
onErrorThrown: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasurementDropdown
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
|
||||||
|
import Dropdown from 'shared/components/Dropdown'
|
||||||
|
import {showTagKeys} from 'shared/apis/metaQuery'
|
||||||
|
import parsers from 'shared/parsing'
|
||||||
|
const {tagKeys: showTagKeysParser} = parsers
|
||||||
|
|
||||||
|
class TagKeyDropdown extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
tagKeys: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getTags = ::this._getTags
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._getTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(nextProps) {
|
||||||
|
if (
|
||||||
|
nextProps.database === this.props.database &&
|
||||||
|
nextProps.measurement === this.props.measurement
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {tagKeys} = this.state
|
||||||
|
const {tagKey, onSelectTagKey, onStartEdit} = this.props
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
items={tagKeys.map(text => ({text}))}
|
||||||
|
selected={tagKey || 'Select Tag Key'}
|
||||||
|
onChoose={onSelectTagKey}
|
||||||
|
onClick={() => onStartEdit(null)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getTags() {
|
||||||
|
const {
|
||||||
|
database,
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
onSelectTagKey,
|
||||||
|
onErrorThrown,
|
||||||
|
} = this.props
|
||||||
|
const {source: {links: {proxy}}} = this.context
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await showTagKeys({source: proxy, database, measurement})
|
||||||
|
const {tagKeys} = showTagKeysParser(data)
|
||||||
|
|
||||||
|
this.setState({tagKeys})
|
||||||
|
const selectedTagKeyText = tagKeys.includes(tagKey)
|
||||||
|
? tagKey
|
||||||
|
: tagKeys[0] || 'No tags'
|
||||||
|
onSelectTagKey({text: selectedTagKeyText})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
onErrorThrown(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
TagKeyDropdown.contextTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
TagKeyDropdown.propTypes = {
|
||||||
|
database: string.isRequired,
|
||||||
|
measurement: string.isRequired,
|
||||||
|
tagKey: string,
|
||||||
|
onSelectTagKey: func.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
onErrorThrown: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagKeyDropdown
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import DatabaseDropdown from 'src/dashboards/components/DatabaseDropdown'
|
||||||
|
import MeasurementDropdown from 'src/dashboards/components/MeasurementDropdown'
|
||||||
|
import TagKeyDropdown from 'src/dashboards/components/TagKeyDropdown'
|
||||||
|
|
||||||
|
const TemplateQueryBuilder = ({
|
||||||
|
selectedType,
|
||||||
|
selectedDatabase,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedTagKey,
|
||||||
|
onSelectDatabase,
|
||||||
|
onSelectMeasurement,
|
||||||
|
onSelectTagKey,
|
||||||
|
onStartEdit,
|
||||||
|
onErrorThrown,
|
||||||
|
}) => {
|
||||||
|
switch (selectedType) {
|
||||||
|
case 'csv':
|
||||||
|
return <div className="tvm-csv-instructions">Enter values below</div>
|
||||||
|
case 'databases':
|
||||||
|
return <div className="tvm-query-builder--text">SHOW DATABASES</div>
|
||||||
|
case 'measurements':
|
||||||
|
return (
|
||||||
|
<div className="tvm-query-builder">
|
||||||
|
<span className="tvm-query-builder--text">SHOW MEASUREMENTS ON</span>
|
||||||
|
<DatabaseDropdown
|
||||||
|
onSelectDatabase={onSelectDatabase}
|
||||||
|
database={selectedDatabase}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'fieldKeys':
|
||||||
|
case 'tagKeys':
|
||||||
|
return (
|
||||||
|
<div className="tvm-query-builder">
|
||||||
|
<span className="tvm-query-builder--text">
|
||||||
|
SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON
|
||||||
|
</span>
|
||||||
|
<DatabaseDropdown
|
||||||
|
onSelectDatabase={onSelectDatabase}
|
||||||
|
database={selectedDatabase}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
<span className="tvm-query-builder--text">FROM</span>
|
||||||
|
{selectedDatabase
|
||||||
|
? <MeasurementDropdown
|
||||||
|
database={selectedDatabase}
|
||||||
|
measurement={selectedMeasurement}
|
||||||
|
onSelectMeasurement={onSelectMeasurement}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
: <div>No database selected</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'tagValues':
|
||||||
|
return (
|
||||||
|
<div className="tvm-query-builder">
|
||||||
|
<span className="tvm-query-builder--text">SHOW TAG VALUES ON</span>
|
||||||
|
<DatabaseDropdown
|
||||||
|
onSelectDatabase={onSelectDatabase}
|
||||||
|
database={selectedDatabase}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
<span className="tvm-query-builder--text">FROM</span>
|
||||||
|
{selectedDatabase
|
||||||
|
? <MeasurementDropdown
|
||||||
|
database={selectedDatabase}
|
||||||
|
measurement={selectedMeasurement}
|
||||||
|
onSelectMeasurement={onSelectMeasurement}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
: 'Pick a DB'}
|
||||||
|
<span className="tvm-query-builder--text">WITH KEY =</span>
|
||||||
|
{selectedMeasurement
|
||||||
|
? <TagKeyDropdown
|
||||||
|
database={selectedDatabase}
|
||||||
|
measurement={selectedMeasurement}
|
||||||
|
tagKey={selectedTagKey}
|
||||||
|
onSelectTagKey={onSelectTagKey}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
: 'Pick a Tag Key'}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return <div><span className="tvm-query-builder--text">n/a</span></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {func, string} = PropTypes
|
||||||
|
|
||||||
|
TemplateQueryBuilder.propTypes = {
|
||||||
|
selectedType: string.isRequired,
|
||||||
|
onSelectDatabase: func.isRequired,
|
||||||
|
onSelectMeasurement: func.isRequired,
|
||||||
|
onSelectTagKey: func.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
selectedMeasurement: string,
|
||||||
|
selectedDatabase: string,
|
||||||
|
selectedTagKey: string,
|
||||||
|
onErrorThrown: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateQueryBuilder
|
|
@ -0,0 +1,243 @@
|
||||||
|
import React, {Component, PropTypes} from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import uuid from 'node-uuid'
|
||||||
|
|
||||||
|
import TemplateVariableTable
|
||||||
|
from 'src/dashboards/components/TemplateVariableTable'
|
||||||
|
|
||||||
|
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
|
||||||
|
|
||||||
|
const TemplateVariableManager = ({
|
||||||
|
onClose,
|
||||||
|
onEditTemplateVariables,
|
||||||
|
source,
|
||||||
|
templates,
|
||||||
|
onRunQuerySuccess,
|
||||||
|
onRunQueryFailure,
|
||||||
|
onSaveTemplatesSuccess,
|
||||||
|
onAddVariable,
|
||||||
|
onDelete,
|
||||||
|
tempVarAlreadyExists,
|
||||||
|
isEdited,
|
||||||
|
}) => (
|
||||||
|
<div className="template-variable-manager">
|
||||||
|
<div className="template-variable-manager--header">
|
||||||
|
<div className="page-header__left">
|
||||||
|
<h1 className="page-header__title">Template Variables</h1>
|
||||||
|
</div>
|
||||||
|
<div className="page-header__right">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
onClick={onAddVariable}
|
||||||
|
>
|
||||||
|
Add Variable
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={classNames('btn btn-success btn-sm', {
|
||||||
|
disabled: !isEdited,
|
||||||
|
})}
|
||||||
|
type="button"
|
||||||
|
onClick={onEditTemplateVariables(templates, onSaveTemplatesSuccess)}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className="page-header__dismiss"
|
||||||
|
onClick={() => onClose(isEdited)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="template-variable-manager--body">
|
||||||
|
<TemplateVariableTable
|
||||||
|
source={source}
|
||||||
|
templates={templates}
|
||||||
|
onRunQuerySuccess={onRunQuerySuccess}
|
||||||
|
onRunQueryFailure={onRunQueryFailure}
|
||||||
|
onDelete={onDelete}
|
||||||
|
tempVarAlreadyExists={tempVarAlreadyExists}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
class TemplateVariableManagerWrapper extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
rows: this.props.templates,
|
||||||
|
isEdited: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onRunQuerySuccess = ::this.onRunQuerySuccess
|
||||||
|
this.onSaveTemplatesSuccess = ::this.onSaveTemplatesSuccess
|
||||||
|
this.onAddVariable = ::this.onAddVariable
|
||||||
|
this.onDeleteTemplateVariable = ::this.onDeleteTemplateVariable
|
||||||
|
this.tempVarAlreadyExists = ::this.tempVarAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddVariable() {
|
||||||
|
const {rows} = this.state
|
||||||
|
|
||||||
|
const newRow = {
|
||||||
|
tempVar: '',
|
||||||
|
values: [],
|
||||||
|
id: uuid.v4(),
|
||||||
|
type: 'csv',
|
||||||
|
query: {
|
||||||
|
influxql: '',
|
||||||
|
db: '',
|
||||||
|
// rp, TODO
|
||||||
|
measurement: '',
|
||||||
|
tagKey: '',
|
||||||
|
},
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRows = [newRow, ...rows]
|
||||||
|
|
||||||
|
this.setState({rows: newRows})
|
||||||
|
}
|
||||||
|
|
||||||
|
onRunQuerySuccess(template, queryConfig, parsedData, tempVar) {
|
||||||
|
const {rows} = this.state
|
||||||
|
const {id, links} = template
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
query: influxql,
|
||||||
|
database: db,
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
} = queryConfig
|
||||||
|
|
||||||
|
// Determine which is the selectedValue, if any
|
||||||
|
const currentRow = rows.find(row => row.id === id)
|
||||||
|
|
||||||
|
let selectedValue
|
||||||
|
if (currentRow && currentRow.values && currentRow.values.length) {
|
||||||
|
const matchedValue = currentRow.values.find(val => val.selected)
|
||||||
|
if (matchedValue) {
|
||||||
|
selectedValue = matchedValue.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!selectedValue &&
|
||||||
|
currentRow &&
|
||||||
|
currentRow.values &&
|
||||||
|
currentRow.values.length
|
||||||
|
) {
|
||||||
|
selectedValue = currentRow.values[0].value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedValue) {
|
||||||
|
selectedValue = parsedData[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = parsedData.map(value => ({
|
||||||
|
value,
|
||||||
|
type: TEMPLATE_VARIABLE_TYPES[type],
|
||||||
|
selected: selectedValue === value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const templateVariable = {
|
||||||
|
tempVar,
|
||||||
|
values,
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
query: {
|
||||||
|
influxql,
|
||||||
|
db,
|
||||||
|
// rp, TODO
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
},
|
||||||
|
links,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRows = rows.map(r => (r.id === template.id ? templateVariable : r))
|
||||||
|
|
||||||
|
this.setState({rows: newRows, isEdited: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveTemplatesSuccess() {
|
||||||
|
const {rows} = this.state
|
||||||
|
|
||||||
|
const newRows = rows.map(row => ({...row, isNew: false}))
|
||||||
|
|
||||||
|
this.setState({rows: newRows, isEdited: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteTemplateVariable(templateID) {
|
||||||
|
const {rows} = this.state
|
||||||
|
|
||||||
|
const newRows = rows.filter(({id}) => id !== templateID)
|
||||||
|
|
||||||
|
this.setState({rows: newRows, isEdited: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
tempVarAlreadyExists(testTempVar, testID) {
|
||||||
|
const {rows: tempVars} = this.state
|
||||||
|
return tempVars.some(
|
||||||
|
({tempVar, id}) => tempVar === testTempVar && id !== testID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {rows, isEdited} = this.state
|
||||||
|
return (
|
||||||
|
<TemplateVariableManager
|
||||||
|
{...this.props}
|
||||||
|
onRunQuerySuccess={this.onRunQuerySuccess}
|
||||||
|
onSaveTemplatesSuccess={this.onSaveTemplatesSuccess}
|
||||||
|
onAddVariable={this.onAddVariable}
|
||||||
|
templates={rows}
|
||||||
|
isEdited={isEdited}
|
||||||
|
onDelete={this.onDeleteTemplateVariable}
|
||||||
|
tempVarAlreadyExists={this.tempVarAlreadyExists}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
TemplateVariableManager.propTypes = {
|
||||||
|
...TemplateVariableManagerWrapper.propTypes,
|
||||||
|
onRunQuerySuccess: func.isRequired,
|
||||||
|
onSaveTemplatesSuccess: func.isRequired,
|
||||||
|
onAddVariable: func.isRequired,
|
||||||
|
isEdited: bool.isRequired,
|
||||||
|
onDelete: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateVariableManagerWrapper.propTypes = {
|
||||||
|
onClose: func.isRequired,
|
||||||
|
onEditTemplateVariables: func.isRequired,
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string,
|
||||||
|
}),
|
||||||
|
}).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
influxql: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
value: string.isRequired,
|
||||||
|
type: string.isRequired,
|
||||||
|
selected: bool.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
onRunQueryFailure: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateVariableManagerWrapper
|
|
@ -0,0 +1,484 @@
|
||||||
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
import {connect} from 'react-redux'
|
||||||
|
import {bindActionCreators} from 'redux'
|
||||||
|
|
||||||
|
import OnClickOutside from 'react-onclickoutside'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import Dropdown from 'shared/components/Dropdown'
|
||||||
|
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
|
||||||
|
import TemplateQueryBuilder
|
||||||
|
from 'src/dashboards/components/TemplateQueryBuilder'
|
||||||
|
|
||||||
|
import {
|
||||||
|
runTemplateVariableQuery as runTemplateVariableQueryAJAX,
|
||||||
|
} from 'src/dashboards/apis'
|
||||||
|
|
||||||
|
import parsers from 'shared/parsing'
|
||||||
|
|
||||||
|
import {TEMPLATE_TYPES} from 'src/dashboards/constants'
|
||||||
|
import generateTemplateVariableQuery
|
||||||
|
from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||||
|
|
||||||
|
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||||
|
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||||
|
|
||||||
|
const RowValues = ({
|
||||||
|
selectedType,
|
||||||
|
values = [],
|
||||||
|
isEditing,
|
||||||
|
onStartEdit,
|
||||||
|
autoFocusTarget,
|
||||||
|
}) => {
|
||||||
|
const _values = values.map(({value}) => value).join(', ')
|
||||||
|
|
||||||
|
if (selectedType === 'csv') {
|
||||||
|
return (
|
||||||
|
<TableInput
|
||||||
|
name="values"
|
||||||
|
defaultValue={_values}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
autoFocusTarget={autoFocusTarget}
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={values.length ? 'tvm-values' : 'tvm-values-empty'}>
|
||||||
|
{values.length ? _values : 'No values to display'}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowButtons = ({
|
||||||
|
onStartEdit,
|
||||||
|
isEditing,
|
||||||
|
onCancelEdit,
|
||||||
|
onDelete,
|
||||||
|
id,
|
||||||
|
selectedType,
|
||||||
|
}) => {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className="tvm-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-info"
|
||||||
|
type="button"
|
||||||
|
onClick={onCancelEdit}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-sm btn-success" type="submit">
|
||||||
|
{selectedType === 'csv' ? 'Save Values' : 'Get Values'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="tvm-actions">
|
||||||
|
<DeleteConfirmButtons onDelete={() => onDelete(id)} />
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-info btn-edit"
|
||||||
|
type="button"
|
||||||
|
onClick={e => {
|
||||||
|
// prevent subsequent 'onSubmit' that is caused by an unknown source,
|
||||||
|
// possible onClickOutside, after 'onClick'. this allows
|
||||||
|
// us to enter 'isEditing' mode
|
||||||
|
e.preventDefault()
|
||||||
|
onStartEdit('tempVar')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="icon pencil" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateVariableRow = ({
|
||||||
|
template: {id, tempVar, values},
|
||||||
|
isEditing,
|
||||||
|
selectedType,
|
||||||
|
selectedDatabase,
|
||||||
|
selectedMeasurement,
|
||||||
|
onSelectType,
|
||||||
|
onSelectDatabase,
|
||||||
|
onSelectMeasurement,
|
||||||
|
selectedTagKey,
|
||||||
|
onSelectTagKey,
|
||||||
|
onStartEdit,
|
||||||
|
onCancelEdit,
|
||||||
|
autoFocusTarget,
|
||||||
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
|
onErrorThrown,
|
||||||
|
}) => (
|
||||||
|
<form
|
||||||
|
className={classNames('template-variable-manager--table-row', {
|
||||||
|
editing: isEditing,
|
||||||
|
})}
|
||||||
|
onSubmit={onSubmit({
|
||||||
|
selectedType,
|
||||||
|
selectedDatabase,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedTagKey,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="tvm--col-1">
|
||||||
|
<TableInput
|
||||||
|
name="tempVar"
|
||||||
|
defaultValue={tempVar}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
autoFocusTarget={autoFocusTarget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tvm--col-2">
|
||||||
|
<Dropdown
|
||||||
|
items={TEMPLATE_TYPES}
|
||||||
|
onChoose={onSelectType}
|
||||||
|
onClick={() => onStartEdit(null)}
|
||||||
|
selected={TEMPLATE_TYPES.find(t => t.type === selectedType).text}
|
||||||
|
className="dropdown-140"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tvm--col-3">
|
||||||
|
<TemplateQueryBuilder
|
||||||
|
onSelectDatabase={onSelectDatabase}
|
||||||
|
selectedType={selectedType}
|
||||||
|
selectedDatabase={selectedDatabase}
|
||||||
|
onSelectMeasurement={onSelectMeasurement}
|
||||||
|
selectedMeasurement={selectedMeasurement}
|
||||||
|
selectedTagKey={selectedTagKey}
|
||||||
|
onSelectTagKey={onSelectTagKey}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onErrorThrown={onErrorThrown}
|
||||||
|
/>
|
||||||
|
<RowValues
|
||||||
|
selectedType={selectedType}
|
||||||
|
values={values}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
autoFocusTarget={autoFocusTarget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tvm--col-4">
|
||||||
|
<RowButtons
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onCancelEdit={onCancelEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
id={id}
|
||||||
|
selectedType={selectedType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TableInput = ({
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
isEditing,
|
||||||
|
onStartEdit,
|
||||||
|
autoFocusTarget,
|
||||||
|
}) => {
|
||||||
|
return isEditing
|
||||||
|
? <div name={name} style={{width: '100%'}}>
|
||||||
|
<input
|
||||||
|
required={true}
|
||||||
|
name={name}
|
||||||
|
autoFocus={name === autoFocusTarget}
|
||||||
|
className="form-control input-sm tvm-input-edit"
|
||||||
|
type="text"
|
||||||
|
defaultValue={
|
||||||
|
name === 'tempVar'
|
||||||
|
? defaultValue.replace(/\u003a/g, '') // remove ':'s
|
||||||
|
: defaultValue
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <div style={{width: '100%'}} onClick={() => onStartEdit(name)}>
|
||||||
|
<div className="tvm-input">{defaultValue}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
class RowWrapper extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
const {template: {type, query, isNew}} = this.props
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditing: !!isNew,
|
||||||
|
isNew: !!isNew,
|
||||||
|
hasBeenSavedToComponentStateOnce: !isNew,
|
||||||
|
selectedType: type,
|
||||||
|
selectedDatabase: query && query.db,
|
||||||
|
selectedMeasurement: query && query.measurement,
|
||||||
|
selectedTagKey: query && query.tagKey,
|
||||||
|
autoFocusTarget: 'tempVar',
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleSubmit = ::this.handleSubmit
|
||||||
|
this.handleSelectType = ::this.handleSelectType
|
||||||
|
this.handleSelectDatabase = ::this.handleSelectDatabase
|
||||||
|
this.handleSelectMeasurement = ::this.handleSelectMeasurement
|
||||||
|
this.handleSelectTagKey = ::this.handleSelectTagKey
|
||||||
|
this.handleStartEdit = ::this.handleStartEdit
|
||||||
|
this.handleCancelEdit = ::this.handleCancelEdit
|
||||||
|
this.runTemplateVariableQuery = ::this.runTemplateVariableQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit({
|
||||||
|
selectedDatabase: database,
|
||||||
|
selectedMeasurement: measurement,
|
||||||
|
selectedTagKey: tagKey,
|
||||||
|
selectedType: type,
|
||||||
|
}) {
|
||||||
|
return async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const {
|
||||||
|
source,
|
||||||
|
template,
|
||||||
|
template: {id},
|
||||||
|
onRunQuerySuccess,
|
||||||
|
onRunQueryFailure,
|
||||||
|
tempVarAlreadyExists,
|
||||||
|
notify,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const _tempVar = e.target.tempVar.value.replace(/\u003a/g, '')
|
||||||
|
const tempVar = `\u003a${_tempVar}\u003a` // add ':'s
|
||||||
|
|
||||||
|
if (tempVarAlreadyExists(tempVar, id)) {
|
||||||
|
return notify(
|
||||||
|
'error',
|
||||||
|
`Variable '${_tempVar}' already exists. Please enter a new value.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isEditing: false,
|
||||||
|
hasBeenSavedToComponentStateOnce: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {query, tempVars} = generateTemplateVariableQuery({
|
||||||
|
type,
|
||||||
|
tempVar,
|
||||||
|
query: {
|
||||||
|
database,
|
||||||
|
// rp, TODO
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryConfig = {
|
||||||
|
type,
|
||||||
|
tempVars,
|
||||||
|
query,
|
||||||
|
database,
|
||||||
|
// rp: TODO
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let parsedData
|
||||||
|
if (type === 'csv') {
|
||||||
|
parsedData = e.target.values.value
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
} else {
|
||||||
|
parsedData = await this.runTemplateVariableQuery(source, queryConfig)
|
||||||
|
}
|
||||||
|
onRunQuerySuccess(template, queryConfig, parsedData, tempVar)
|
||||||
|
} catch (error) {
|
||||||
|
onRunQueryFailure(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOutside() {
|
||||||
|
this.setState({isEditing: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartEdit(name) {
|
||||||
|
this.setState({isEditing: true, autoFocusTarget: name})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelEdit() {
|
||||||
|
const {
|
||||||
|
template: {type, query: {db, measurement, tagKey}, id},
|
||||||
|
onDelete,
|
||||||
|
} = this.props
|
||||||
|
const {hasBeenSavedToComponentStateOnce} = this.state
|
||||||
|
|
||||||
|
if (!hasBeenSavedToComponentStateOnce) {
|
||||||
|
return onDelete(id)
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedType: type,
|
||||||
|
selectedDatabase: db,
|
||||||
|
selectedMeasurement: measurement,
|
||||||
|
selectedTagKey: tagKey,
|
||||||
|
isEditing: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectType(item) {
|
||||||
|
this.setState({
|
||||||
|
selectedType: item.type,
|
||||||
|
selectedDatabase: null,
|
||||||
|
selectedMeasurement: null,
|
||||||
|
selectedTagKey: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectDatabase(item) {
|
||||||
|
this.setState({selectedDatabase: item.text})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectMeasurement(item) {
|
||||||
|
this.setState({selectedMeasurement: item.text})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectTagKey(item) {
|
||||||
|
this.setState({selectedTagKey: item.text})
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTemplateVariableQuery(
|
||||||
|
source,
|
||||||
|
{query, database, rp, tempVars, type, measurement, tagKey}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const {data} = await runTemplateVariableQueryAJAX(source, {
|
||||||
|
query,
|
||||||
|
db: database,
|
||||||
|
rp,
|
||||||
|
tempVars,
|
||||||
|
})
|
||||||
|
const parsedData = parsers[type](data, tagKey || measurement) // tagKey covers tagKey and fieldKey
|
||||||
|
if (parsedData.errors.length) {
|
||||||
|
throw parsedData.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedData[type]
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isEditing,
|
||||||
|
selectedType,
|
||||||
|
selectedDatabase,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedTagKey,
|
||||||
|
autoFocusTarget,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TemplateVariableRow
|
||||||
|
{...this.props}
|
||||||
|
isEditing={isEditing}
|
||||||
|
selectedType={selectedType}
|
||||||
|
selectedDatabase={selectedDatabase}
|
||||||
|
selectedMeasurement={selectedMeasurement}
|
||||||
|
selectedTagKey={selectedTagKey}
|
||||||
|
onSelectType={this.handleSelectType}
|
||||||
|
onSelectDatabase={this.handleSelectDatabase}
|
||||||
|
onSelectMeasurement={this.handleSelectMeasurement}
|
||||||
|
onSelectTagKey={this.handleSelectTagKey}
|
||||||
|
onStartEdit={this.handleStartEdit}
|
||||||
|
onCancelEdit={this.handleCancelEdit}
|
||||||
|
autoFocusTarget={autoFocusTarget}
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
RowWrapper.propTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string,
|
||||||
|
}),
|
||||||
|
}).isRequired,
|
||||||
|
template: shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
influxql: string,
|
||||||
|
measurement: string,
|
||||||
|
tagKey: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
value: string.isRequired,
|
||||||
|
type: string.isRequired,
|
||||||
|
selected: bool.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
links: shape({
|
||||||
|
self: string.isRequired,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onRunQuerySuccess: func.isRequired,
|
||||||
|
onRunQueryFailure: func.isRequired,
|
||||||
|
onDelete: func.isRequired,
|
||||||
|
tempVarAlreadyExists: func.isRequired,
|
||||||
|
notify: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateVariableRow.propTypes = {
|
||||||
|
...RowWrapper.propTypes,
|
||||||
|
selectedType: string.isRequired,
|
||||||
|
selectedDatabase: string,
|
||||||
|
selectedTagKey: string,
|
||||||
|
onSelectType: func.isRequired,
|
||||||
|
onSelectDatabase: func.isRequired,
|
||||||
|
onSelectTagKey: func.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
onCancelEdit: func.isRequired,
|
||||||
|
onSubmit: func.isRequired,
|
||||||
|
onErrorThrown: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
TableInput.propTypes = {
|
||||||
|
defaultValue: string,
|
||||||
|
isEditing: bool.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
autoFocusTarget: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
RowValues.propTypes = {
|
||||||
|
selectedType: string.isRequired,
|
||||||
|
values: arrayOf(shape()),
|
||||||
|
isEditing: bool.isRequired,
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
autoFocusTarget: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
RowButtons.propTypes = {
|
||||||
|
onStartEdit: func.isRequired,
|
||||||
|
isEditing: bool.isRequired,
|
||||||
|
onCancelEdit: func.isRequired,
|
||||||
|
onDelete: func.isRequired,
|
||||||
|
id: string.isRequired,
|
||||||
|
selectedType: string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onErrorThrown: bindActionCreators(errorThrownAction, dispatch),
|
||||||
|
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(OnClickOutside(RowWrapper))
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
|
import TemplateVariableRow from 'src/dashboards/components/TemplateVariableRow'
|
||||||
|
|
||||||
|
const TemplateVariableTable = ({
|
||||||
|
source,
|
||||||
|
templates,
|
||||||
|
onRunQuerySuccess,
|
||||||
|
onRunQueryFailure,
|
||||||
|
onDelete,
|
||||||
|
tempVarAlreadyExists,
|
||||||
|
}) => (
|
||||||
|
<div className="template-variable-manager--table">
|
||||||
|
{templates.length
|
||||||
|
? <div className="template-variable-manager--table-container">
|
||||||
|
<div className="template-variable-manager--table-heading">
|
||||||
|
<div className="tvm--col-1">Variable</div>
|
||||||
|
<div className="tvm--col-2">Type</div>
|
||||||
|
<div className="tvm--col-3">Definition / Values</div>
|
||||||
|
<div className="tvm--col-4" />
|
||||||
|
</div>
|
||||||
|
<div className="template-variable-manager--table-rows">
|
||||||
|
{templates.map(t => (
|
||||||
|
<TemplateVariableRow
|
||||||
|
key={t.id}
|
||||||
|
source={source}
|
||||||
|
template={t}
|
||||||
|
onRunQuerySuccess={onRunQuerySuccess}
|
||||||
|
onRunQueryFailure={onRunQueryFailure}
|
||||||
|
onDelete={onDelete}
|
||||||
|
tempVarAlreadyExists={tempVarAlreadyExists}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <div className="generic-empty-state">
|
||||||
|
<h4 style={{margin: '60px 0'}} className="no-user-select">You have no Template Variables, why not create one?</h4>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
TemplateVariableTable.propTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string,
|
||||||
|
}),
|
||||||
|
}).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
influxql: string,
|
||||||
|
measurement: string,
|
||||||
|
tagKey: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
value: string.isRequired,
|
||||||
|
type: string.isRequired,
|
||||||
|
selected: bool.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
onRunQuerySuccess: func.isRequired,
|
||||||
|
onRunQueryFailure: func.isRequired,
|
||||||
|
onDelete: func.isRequired,
|
||||||
|
tempVarAlreadyExists: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateVariableTable
|
|
@ -26,3 +26,49 @@ export const NEW_DASHBOARD = {
|
||||||
name: 'Name This Dashboard',
|
name: 'Name This Dashboard',
|
||||||
cells: [NEW_DEFAULT_DASHBOARD_CELL],
|
cells: [NEW_DEFAULT_DASHBOARD_CELL],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_TYPES = [
|
||||||
|
{
|
||||||
|
text: 'CSV',
|
||||||
|
type: 'csv',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Databases',
|
||||||
|
type: 'databases',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Measurements',
|
||||||
|
type: 'measurements',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Field Keys',
|
||||||
|
type: 'fieldKeys',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Tag Keys',
|
||||||
|
type: 'tagKeys',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Tag Values',
|
||||||
|
type: 'tagValues',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TEMPLATE_VARIABLE_TYPES = {
|
||||||
|
csv: 'csv',
|
||||||
|
databases: 'database',
|
||||||
|
measurements: 'measurement',
|
||||||
|
fieldKeys: 'fieldKey',
|
||||||
|
tagKeys: 'tagKey',
|
||||||
|
tagValues: 'tagValue',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_VARIABLE_QUERIES = {
|
||||||
|
databases: 'SHOW DATABASES',
|
||||||
|
measurements: 'SHOW MEASUREMENTS ON :database:',
|
||||||
|
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
|
||||||
|
tagKeys: 'SHOW TAG KEYS ON :database: FROM :measurement:',
|
||||||
|
tagValues: 'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_MATCHER = /\B:\B|:\w+\b(?!:)/g
|
||||||
|
|
|
@ -1,85 +1,54 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes, Component} from 'react'
|
||||||
import {Link} from 'react-router'
|
import {Link} from 'react-router'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {bindActionCreators} from 'redux'
|
import {bindActionCreators} from 'redux'
|
||||||
|
|
||||||
|
import OverlayTechnologies from 'src/shared/components/OverlayTechnologies'
|
||||||
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
|
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
|
||||||
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
|
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
|
||||||
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
|
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
|
||||||
import Dashboard from 'src/dashboards/components/Dashboard'
|
import Dashboard from 'src/dashboards/components/Dashboard'
|
||||||
|
import TemplateVariableManager
|
||||||
|
from 'src/dashboards/components/TemplateVariableManager'
|
||||||
|
|
||||||
|
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||||
|
|
||||||
import * as dashboardActionCreators from 'src/dashboards/actions'
|
import * as dashboardActionCreators from 'src/dashboards/actions'
|
||||||
|
|
||||||
import {setAutoRefresh} from 'shared/actions/app'
|
import {setAutoRefresh} from 'shared/actions/app'
|
||||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||||
|
|
||||||
const {
|
class DashboardPage extends Component {
|
||||||
arrayOf,
|
constructor(props) {
|
||||||
bool,
|
super(props)
|
||||||
func,
|
|
||||||
number,
|
|
||||||
shape,
|
|
||||||
string,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
const DashboardPage = React.createClass({
|
this.state = {
|
||||||
propTypes: {
|
|
||||||
source: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string,
|
|
||||||
self: string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
params: shape({
|
|
||||||
sourceID: string.isRequired,
|
|
||||||
dashboardID: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
location: shape({
|
|
||||||
pathname: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
dashboardActions: shape({
|
|
||||||
putDashboard: func.isRequired,
|
|
||||||
getDashboardsAsync: func.isRequired,
|
|
||||||
setTimeRange: func.isRequired,
|
|
||||||
addDashboardCellAsync: func.isRequired,
|
|
||||||
editDashboardCell: func.isRequired,
|
|
||||||
renameDashboardCell: func.isRequired,
|
|
||||||
editQueryStatus: func,
|
|
||||||
}).isRequired,
|
|
||||||
dashboards: arrayOf(shape({
|
|
||||||
id: number.isRequired,
|
|
||||||
cells: arrayOf(shape({})).isRequired,
|
|
||||||
})),
|
|
||||||
handleChooseAutoRefresh: func.isRequired,
|
|
||||||
autoRefresh: number.isRequired,
|
|
||||||
timeRange: shape({}).isRequired,
|
|
||||||
inPresentationMode: bool.isRequired,
|
|
||||||
handleClickPresentationButton: func,
|
|
||||||
cellQueryStatus: shape({
|
|
||||||
queryID: string,
|
|
||||||
status: shape(),
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
childContextTypes: {
|
|
||||||
source: shape({
|
|
||||||
links: shape({
|
|
||||||
proxy: string.isRequired,
|
|
||||||
self: string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {source: this.props.source}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
selectedCell: null,
|
selectedCell: null,
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
|
isTemplating: false,
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
this.handleAddCell = ::this.handleAddCell
|
||||||
|
this.handleEditDashboard = ::this.handleEditDashboard
|
||||||
|
this.handleSaveEditedCell = ::this.handleSaveEditedCell
|
||||||
|
this.handleDismissOverlay = ::this.handleDismissOverlay
|
||||||
|
this.handleUpdatePosition = ::this.handleUpdatePosition
|
||||||
|
this.handleChooseTimeRange = ::this.handleChooseTimeRange
|
||||||
|
this.handleRenameDashboard = ::this.handleRenameDashboard
|
||||||
|
this.handleEditDashboardCell = ::this.handleEditDashboardCell
|
||||||
|
this.handleCancelEditDashboard = ::this.handleCancelEditDashboard
|
||||||
|
this.handleDeleteDashboardCell = ::this.handleDeleteDashboardCell
|
||||||
|
this.handleOpenTemplateManager = ::this.handleOpenTemplateManager
|
||||||
|
this.handleRenameDashboardCell = ::this.handleRenameDashboardCell
|
||||||
|
this.handleUpdateDashboardCell = ::this.handleUpdateDashboardCell
|
||||||
|
this.handleCloseTemplateManager = ::this.handleCloseTemplateManager
|
||||||
|
this.handleSummonOverlayTechnologies = ::this
|
||||||
|
.handleSummonOverlayTechnologies
|
||||||
|
this.handleRunTemplateVariableQuery = ::this.handleRunTemplateVariableQuery
|
||||||
|
this.handleSelectTemplate = ::this.handleSelectTemplate
|
||||||
|
this.handleEditTemplateVariables = ::this.handleEditTemplateVariables
|
||||||
|
this.handleRunQueryFailure = ::this.handleRunQueryFailure
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
|
@ -88,78 +57,159 @@ const DashboardPage = React.createClass({
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
getDashboardsAsync(dashboardID)
|
getDashboardsAsync(dashboardID)
|
||||||
},
|
}
|
||||||
|
|
||||||
|
handleOpenTemplateManager() {
|
||||||
|
this.setState({isTemplating: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseTemplateManager(isEdited) {
|
||||||
|
if (
|
||||||
|
!isEdited ||
|
||||||
|
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
|
||||||
|
) {
|
||||||
|
this.setState({isTemplating: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleDismissOverlay() {
|
handleDismissOverlay() {
|
||||||
this.setState({selectedCell: null})
|
this.setState({selectedCell: null})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleSaveEditedCell(newCell) {
|
handleSaveEditedCell(newCell) {
|
||||||
this.props.dashboardActions.updateDashboardCell(this.getActiveDashboard(), newCell)
|
this.props.dashboardActions
|
||||||
.then(this.handleDismissOverlay)
|
.updateDashboardCell(this.getActiveDashboard(), newCell)
|
||||||
},
|
.then(this.handleDismissOverlay)
|
||||||
|
}
|
||||||
|
|
||||||
handleSummonOverlayTechnologies(cell) {
|
handleSummonOverlayTechnologies(cell) {
|
||||||
this.setState({selectedCell: cell})
|
this.setState({selectedCell: cell})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleChooseTimeRange({lower}) {
|
handleChooseTimeRange({lower}) {
|
||||||
this.props.dashboardActions.setTimeRange({lower, upper: null})
|
this.props.dashboardActions.setTimeRange({lower, upper: null})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleUpdatePosition(cells) {
|
handleUpdatePosition(cells) {
|
||||||
const newDashboard = {...this.getActiveDashboard(), cells}
|
const newDashboard = {...this.getActiveDashboard(), cells}
|
||||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
this.props.dashboardActions.updateDashboard(newDashboard)
|
||||||
this.props.dashboardActions.putDashboard(newDashboard)
|
this.props.dashboardActions.putDashboard(newDashboard)
|
||||||
},
|
}
|
||||||
|
|
||||||
handleAddCell() {
|
handleAddCell() {
|
||||||
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
|
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
|
||||||
},
|
}
|
||||||
|
|
||||||
handleEditDashboard() {
|
handleEditDashboard() {
|
||||||
this.setState({isEditMode: true})
|
this.setState({isEditMode: true})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleCancelEditDashboard() {
|
handleCancelEditDashboard() {
|
||||||
this.setState({isEditMode: false})
|
this.setState({isEditMode: false})
|
||||||
},
|
}
|
||||||
|
|
||||||
handleRenameDashboard(name) {
|
handleRenameDashboard(name) {
|
||||||
this.setState({isEditMode: false})
|
this.setState({isEditMode: false})
|
||||||
const newDashboard = {...this.getActiveDashboard(), name}
|
const newDashboard = {...this.getActiveDashboard(), name}
|
||||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
this.props.dashboardActions.updateDashboard(newDashboard)
|
||||||
this.props.dashboardActions.putDashboard(newDashboard)
|
this.props.dashboardActions.putDashboard(newDashboard)
|
||||||
},
|
}
|
||||||
|
|
||||||
// Places cell into editing mode.
|
// Places cell into editing mode.
|
||||||
handleEditDashboardCell(x, y, isEditing) {
|
handleEditDashboardCell(x, y, isEditing) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.dashboardActions.editDashboardCell(this.getActiveDashboard(), x, y, !isEditing) /* eslint-disable no-negated-condition */
|
this.props.dashboardActions.editDashboardCell(
|
||||||
|
this.getActiveDashboard(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
!isEditing
|
||||||
|
) /* eslint-disable no-negated-condition */
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
handleRenameDashboardCell(x, y) {
|
handleRenameDashboardCell(x, y) {
|
||||||
return (evt) => {
|
return evt => {
|
||||||
this.props.dashboardActions.renameDashboardCell(this.getActiveDashboard(), x, y, evt.target.value)
|
this.props.dashboardActions.renameDashboardCell(
|
||||||
|
this.getActiveDashboard(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
evt.target.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
handleUpdateDashboardCell(newCell) {
|
handleUpdateDashboardCell(newCell) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.dashboardActions.editDashboardCell(this.getActiveDashboard(), newCell.x, newCell.y, false)
|
this.props.dashboardActions.editDashboardCell(
|
||||||
|
this.getActiveDashboard(),
|
||||||
|
newCell.x,
|
||||||
|
newCell.y,
|
||||||
|
false
|
||||||
|
)
|
||||||
this.props.dashboardActions.putDashboard(this.getActiveDashboard())
|
this.props.dashboardActions.putDashboard(this.getActiveDashboard())
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
handleDeleteDashboardCell(cell) {
|
handleDeleteDashboardCell(cell) {
|
||||||
this.props.dashboardActions.deleteDashboardCellAsync(cell)
|
const dashboard = this.getActiveDashboard()
|
||||||
},
|
this.props.dashboardActions.deleteDashboardCellAsync(dashboard, cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectTemplate(templateID, values) {
|
||||||
|
const {params: {dashboardID}} = this.props
|
||||||
|
this.props.dashboardActions.templateVariableSelected(
|
||||||
|
+dashboardID,
|
||||||
|
templateID,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRunTemplateVariableQuery(
|
||||||
|
templateVariable,
|
||||||
|
{query, db, tempVars, type, tagKey, measurement}
|
||||||
|
) {
|
||||||
|
const {source} = this.props
|
||||||
|
this.props.dashboardActions.runTemplateVariableQueryAsync(
|
||||||
|
templateVariable,
|
||||||
|
{
|
||||||
|
source,
|
||||||
|
query,
|
||||||
|
db,
|
||||||
|
// rp, TODO
|
||||||
|
tempVars,
|
||||||
|
type,
|
||||||
|
tagKey,
|
||||||
|
measurement,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditTemplateVariables(templates, onSaveTemplatesSuccess) {
|
||||||
|
return async () => {
|
||||||
|
const {params: {dashboardID}, dashboards} = this.props
|
||||||
|
const currentDashboard = dashboards.find(({id}) => id === +dashboardID)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.props.dashboardActions.putDashboard({
|
||||||
|
...currentDashboard,
|
||||||
|
templates,
|
||||||
|
})
|
||||||
|
onSaveTemplatesSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRunQueryFailure(error) {
|
||||||
|
console.error(error)
|
||||||
|
this.props.errorThrown(error)
|
||||||
|
}
|
||||||
|
|
||||||
getActiveDashboard() {
|
getActiveDashboard() {
|
||||||
const {params: {dashboardID}, dashboards} = this.props
|
const {params: {dashboardID}, dashboards} = this.props
|
||||||
return dashboards.find(d => d.id === +dashboardID)
|
return dashboards.find(d => d.id === +dashboardID)
|
||||||
},
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
@ -177,35 +227,41 @@ const DashboardPage = React.createClass({
|
||||||
|
|
||||||
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
||||||
|
|
||||||
const {
|
const {selectedCell, isEditMode, isTemplating} = this.state
|
||||||
selectedCell,
|
|
||||||
isEditMode,
|
|
||||||
} = this.state
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
{
|
{isTemplating
|
||||||
selectedCell ?
|
? <OverlayTechnologies>
|
||||||
<CellEditorOverlay
|
<TemplateVariableManager
|
||||||
|
onClose={this.handleCloseTemplateManager}
|
||||||
|
onEditTemplateVariables={this.handleEditTemplateVariables}
|
||||||
|
source={source}
|
||||||
|
templates={dashboard.templates}
|
||||||
|
onRunQueryFailure={this.handleRunQueryFailure}
|
||||||
|
/>
|
||||||
|
</OverlayTechnologies>
|
||||||
|
: null}
|
||||||
|
{selectedCell
|
||||||
|
? <CellEditorOverlay
|
||||||
source={source}
|
source={source}
|
||||||
|
templates={dashboard.templates}
|
||||||
cell={selectedCell}
|
cell={selectedCell}
|
||||||
autoRefresh={autoRefresh}
|
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
onCancel={this.handleDismissOverlay}
|
autoRefresh={autoRefresh}
|
||||||
onSave={this.handleSaveEditedCell}
|
|
||||||
editQueryStatus={dashboardActions.editCellQueryStatus}
|
|
||||||
queryStatus={cellQueryStatus}
|
queryStatus={cellQueryStatus}
|
||||||
/> :
|
onSave={this.handleSaveEditedCell}
|
||||||
null
|
onCancel={this.handleDismissOverlay}
|
||||||
}
|
editQueryStatus={dashboardActions.editCellQueryStatus}
|
||||||
{
|
/>
|
||||||
isEditMode ?
|
: null}
|
||||||
<DashboardHeaderEdit
|
{isEditMode
|
||||||
|
? <DashboardHeaderEdit
|
||||||
dashboard={dashboard}
|
dashboard={dashboard}
|
||||||
onCancel={this.handleCancelEditDashboard}
|
onCancel={this.handleCancelEditDashboard}
|
||||||
onSave={this.handleRenameDashboard}
|
onSave={this.handleRenameDashboard}
|
||||||
/> :
|
/>
|
||||||
<DashboardHeader
|
: <DashboardHeader
|
||||||
buttonText={dashboard ? dashboard.name : ''}
|
buttonText={dashboard ? dashboard.name : ''}
|
||||||
handleChooseAutoRefresh={handleChooseAutoRefresh}
|
handleChooseAutoRefresh={handleChooseAutoRefresh}
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
|
@ -219,55 +275,106 @@ const DashboardPage = React.createClass({
|
||||||
onAddCell={this.handleAddCell}
|
onAddCell={this.handleAddCell}
|
||||||
onEditDashboard={this.handleEditDashboard}
|
onEditDashboard={this.handleEditDashboard}
|
||||||
>
|
>
|
||||||
{
|
{dashboards
|
||||||
dashboards ?
|
? dashboards.map((d, i) => (
|
||||||
dashboards.map((d, i) => {
|
|
||||||
return (
|
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
|
<Link
|
||||||
|
to={`/sources/${sourceID}/dashboards/${d.id}`}
|
||||||
|
className="role-option"
|
||||||
|
>
|
||||||
{d.name}
|
{d.name}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)
|
))
|
||||||
}) :
|
: null}
|
||||||
null
|
</DashboardHeader>}
|
||||||
}
|
{dashboard
|
||||||
</DashboardHeader>
|
? <Dashboard
|
||||||
}
|
source={source}
|
||||||
{
|
dashboard={dashboard}
|
||||||
dashboard ?
|
timeRange={timeRange}
|
||||||
<Dashboard
|
autoRefresh={autoRefresh}
|
||||||
dashboard={dashboard}
|
onAddCell={this.handleAddCell}
|
||||||
inPresentationMode={inPresentationMode}
|
inPresentationMode={inPresentationMode}
|
||||||
source={source}
|
onEditCell={this.handleEditDashboardCell}
|
||||||
autoRefresh={autoRefresh}
|
onPositionChange={this.handleUpdatePosition}
|
||||||
timeRange={timeRange}
|
onDeleteCell={this.handleDeleteDashboardCell}
|
||||||
onAddCell={this.handleAddCell}
|
onRenameCell={this.handleRenameDashboardCell}
|
||||||
onPositionChange={this.handleUpdatePosition}
|
onUpdateCell={this.handleUpdateDashboardCell}
|
||||||
onEditCell={this.handleEditDashboardCell}
|
onOpenTemplateManager={this.handleOpenTemplateManager}
|
||||||
onRenameCell={this.handleRenameDashboardCell}
|
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||||
onUpdateCell={this.handleUpdateDashboardCell}
|
onSelectTemplate={this.handleSelectTemplate}
|
||||||
onDeleteCell={this.handleDeleteDashboardCell}
|
/>
|
||||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
: null}
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||||
|
|
||||||
|
DashboardPage.propTypes = {
|
||||||
|
source: shape({
|
||||||
|
links: shape({
|
||||||
|
proxy: string,
|
||||||
|
self: string,
|
||||||
|
}),
|
||||||
|
}).isRequired,
|
||||||
|
params: shape({
|
||||||
|
sourceID: string.isRequired,
|
||||||
|
dashboardID: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: shape({
|
||||||
|
pathname: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
dashboardActions: shape({
|
||||||
|
putDashboard: func.isRequired,
|
||||||
|
getDashboardsAsync: func.isRequired,
|
||||||
|
setTimeRange: func.isRequired,
|
||||||
|
addDashboardCellAsync: func.isRequired,
|
||||||
|
editDashboardCell: func.isRequired,
|
||||||
|
renameDashboardCell: func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
dashboards: arrayOf(
|
||||||
|
shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
cells: arrayOf(shape({})).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
rp: string,
|
||||||
|
influxql: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
value: string.isRequired,
|
||||||
|
selected: bool.isRequired,
|
||||||
|
type: string.isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handleChooseAutoRefresh: func.isRequired,
|
||||||
|
autoRefresh: number.isRequired,
|
||||||
|
timeRange: shape({}).isRequired,
|
||||||
|
inPresentationMode: bool.isRequired,
|
||||||
|
handleClickPresentationButton: func,
|
||||||
|
cellQueryStatus: shape({
|
||||||
|
queryID: string,
|
||||||
|
status: shape(),
|
||||||
|
}).isRequired,
|
||||||
|
errorThrown: func,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
const {
|
const {
|
||||||
app: {
|
app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}},
|
||||||
ephemeral: {inPresentationMode},
|
dashboardUI: {dashboards, timeRange, cellQueryStatus},
|
||||||
persisted: {autoRefresh},
|
|
||||||
},
|
|
||||||
dashboardUI: {
|
|
||||||
dashboards,
|
|
||||||
timeRange,
|
|
||||||
cellQueryStatus,
|
|
||||||
},
|
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -279,10 +386,11 @@ const mapStateToProps = (state) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
||||||
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
||||||
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
|
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
|
||||||
|
errorThrown: bindActionCreators(errorThrownAction, dispatch),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage)
|
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage)
|
||||||
|
|
|
@ -10,6 +10,8 @@ const initialState = {
|
||||||
cellQueryStatus: {queryID: null, status: null},
|
cellQueryStatus: {queryID: null, status: null},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||||
|
|
||||||
export default function ui(state = initialState, action) {
|
export default function ui(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'LOAD_DASHBOARDS': {
|
case 'LOAD_DASHBOARDS': {
|
||||||
|
@ -30,8 +32,9 @@ export default function ui(state = initialState, action) {
|
||||||
case 'UPDATE_DASHBOARD': {
|
case 'UPDATE_DASHBOARD': {
|
||||||
const {dashboard} = action.payload
|
const {dashboard} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboard,
|
dashboards: state.dashboards.map(
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? dashboard : d),
|
d => (d.id === dashboard.id ? dashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -40,7 +43,7 @@ export default function ui(state = initialState, action) {
|
||||||
case 'DELETE_DASHBOARD': {
|
case 'DELETE_DASHBOARD': {
|
||||||
const {dashboard} = action.payload
|
const {dashboard} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.filter((d) => d.id !== dashboard.id),
|
dashboards: state.dashboards.filter(d => d.id !== dashboard.id),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -49,10 +52,7 @@ export default function ui(state = initialState, action) {
|
||||||
case 'DELETE_DASHBOARD_FAILED': {
|
case 'DELETE_DASHBOARD_FAILED': {
|
||||||
const {dashboard} = action.payload
|
const {dashboard} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: [
|
dashboards: [_.cloneDeep(dashboard), ...state.dashboards],
|
||||||
_.cloneDeep(dashboard),
|
|
||||||
...state.dashboards,
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,9 @@ export default function ui(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -78,7 +80,9 @@ export default function ui(state = initialState, action) {
|
||||||
|
|
||||||
const newCells = [cell, ...dashboard.cells]
|
const newCells = [cell, ...dashboard.cells]
|
||||||
const newDashboard = {...dashboard, cells: newCells}
|
const newDashboard = {...dashboard, cells: newCells}
|
||||||
const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d)
|
const newDashboards = dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
)
|
||||||
const newState = {dashboards: newDashboards}
|
const newState = {dashboards: newDashboards}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -87,7 +91,7 @@ export default function ui(state = initialState, action) {
|
||||||
case 'EDIT_DASHBOARD_CELL': {
|
case 'EDIT_DASHBOARD_CELL': {
|
||||||
const {x, y, isEditing, dashboard} = action.payload
|
const {x, y, isEditing, dashboard} = action.payload
|
||||||
|
|
||||||
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
|
const cell = dashboard.cells.find(c => c.x === x && c.y === y)
|
||||||
|
|
||||||
const newCell = {
|
const newCell = {
|
||||||
...cell,
|
...cell,
|
||||||
|
@ -96,27 +100,32 @@ export default function ui(state = initialState, action) {
|
||||||
|
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
cells: dashboard.cells.map((c) => c.x === x && c.y === y ? newCell : c),
|
cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'DELETE_DASHBOARD_CELL': {
|
case 'DELETE_DASHBOARD_CELL': {
|
||||||
const {cell} = action.payload
|
const {dashboard, cell} = action.payload
|
||||||
const {dashboard} = state
|
|
||||||
|
|
||||||
const newCells = dashboard.cells.filter((c) => !(c.x === cell.x && c.y === cell.y))
|
const newCells = dashboard.cells.filter(
|
||||||
|
c => !(c.x === cell.x && c.y === cell.y)
|
||||||
|
)
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
cells: newCells,
|
cells: newCells,
|
||||||
}
|
}
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -127,11 +136,15 @@ export default function ui(state = initialState, action) {
|
||||||
|
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
cells: dashboard.cells.map((c) => c.x === cell.x && c.y === cell.y ? cell : c),
|
cells: dashboard.cells.map(
|
||||||
|
c => (c.x === cell.x && c.y === cell.y ? cell : c)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -140,7 +153,7 @@ export default function ui(state = initialState, action) {
|
||||||
case 'RENAME_DASHBOARD_CELL': {
|
case 'RENAME_DASHBOARD_CELL': {
|
||||||
const {x, y, name, dashboard} = action.payload
|
const {x, y, name, dashboard} = action.payload
|
||||||
|
|
||||||
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
|
const cell = dashboard.cells.find(c => c.x === x && c.y === y)
|
||||||
|
|
||||||
const newCell = {
|
const newCell = {
|
||||||
...cell,
|
...cell,
|
||||||
|
@ -149,11 +162,13 @@ export default function ui(state = initialState, action) {
|
||||||
|
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
cells: dashboard.cells.map((c) => c.x === x && c.y === y ? newCell : c),
|
cells: dashboard.cells.map(c => (c.x === x && c.y === y ? newCell : c)),
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map(
|
||||||
|
d => (d.id === dashboard.id ? newDashboard : d)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
|
@ -164,6 +179,37 @@ export default function ui(state = initialState, action) {
|
||||||
|
|
||||||
return {...state, cellQueryStatus: {queryID, status}}
|
return {...state, cellQueryStatus: {queryID, status}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case TEMPLATE_VARIABLE_SELECTED: {
|
||||||
|
const {
|
||||||
|
dashboardID,
|
||||||
|
templateID,
|
||||||
|
values: updatedSelectedValues,
|
||||||
|
} = action.payload
|
||||||
|
const newDashboards = state.dashboards.map(dashboard => {
|
||||||
|
if (dashboard.id === dashboardID) {
|
||||||
|
const newTemplates = dashboard.templates.map(staleTemplate => {
|
||||||
|
if (staleTemplate.id === templateID) {
|
||||||
|
const newValues = staleTemplate.values.map(staleValue => {
|
||||||
|
let selected = false
|
||||||
|
for (let i = 0; i < updatedSelectedValues.length; i++) {
|
||||||
|
if (updatedSelectedValues[i].value === staleValue.value) {
|
||||||
|
selected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {...staleValue, selected}
|
||||||
|
})
|
||||||
|
return {...staleTemplate, values: newValues}
|
||||||
|
}
|
||||||
|
return staleTemplate
|
||||||
|
})
|
||||||
|
return {...dashboard, templates: newTemplates}
|
||||||
|
}
|
||||||
|
return dashboard
|
||||||
|
})
|
||||||
|
return {...state, dashboards: newDashboards}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
|
||||||
|
|
||||||
|
const generateTemplateVariableQuery = ({
|
||||||
|
type,
|
||||||
|
query: {
|
||||||
|
database,
|
||||||
|
// rp, TODO
|
||||||
|
measurement,
|
||||||
|
tagKey,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
const tempVars = []
|
||||||
|
|
||||||
|
if (database) {
|
||||||
|
tempVars.push({
|
||||||
|
tempVar: ':database:',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'database',
|
||||||
|
value: database,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (measurement) {
|
||||||
|
tempVars.push({
|
||||||
|
tempVar: ':measurement:',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'measurement',
|
||||||
|
value: measurement,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (tagKey) {
|
||||||
|
tempVars.push({
|
||||||
|
tempVar: ':tagKey:',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
type: 'tagKey',
|
||||||
|
value: tagKey,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = TEMPLATE_VARIABLE_QUERIES[type]
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
tempVars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateTemplateVariableQuery
|
|
@ -7,11 +7,7 @@ import TagList from './TagList'
|
||||||
import QueryEditor from './QueryEditor'
|
import QueryEditor from './QueryEditor'
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import buildInfluxQLQuery from 'utils/influxql'
|
||||||
|
|
||||||
const {
|
const {arrayOf, func, shape, string} = PropTypes
|
||||||
string,
|
|
||||||
shape,
|
|
||||||
func,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
const QueryBuilder = React.createClass({
|
const QueryBuilder = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -27,6 +23,11 @@ const QueryBuilder = React.createClass({
|
||||||
upper: string,
|
upper: string,
|
||||||
lower: string,
|
lower: string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
actions: shape({
|
actions: shape({
|
||||||
chooseNamespace: func.isRequired,
|
chooseNamespace: func.isRequired,
|
||||||
chooseMeasurement: func.isRequired,
|
chooseMeasurement: func.isRequired,
|
||||||
|
@ -78,12 +79,12 @@ const QueryBuilder = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {query, timeRange} = this.props
|
const {query, timeRange, templates} = this.props
|
||||||
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
|
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-maker--tab-contents">
|
<div className="query-maker--tab-contents">
|
||||||
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
|
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} templates={templates} />
|
||||||
{this.renderLists()}
|
{this.renderLists()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,66 +1,195 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes, Component} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import Dropdown from 'src/shared/components/Dropdown'
|
import Dropdown from 'src/shared/components/Dropdown'
|
||||||
import LoadingDots from 'src/shared/components/LoadingDots'
|
import LoadingDots from 'src/shared/components/LoadingDots'
|
||||||
|
import TemplateDrawer from 'src/shared/components/TemplateDrawer'
|
||||||
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
||||||
|
import {TEMPLATE_MATCHER} from 'src/dashboards/constants'
|
||||||
|
|
||||||
const ENTER = 13
|
class QueryEditor extends Component {
|
||||||
const ESCAPE = 27
|
constructor(props) {
|
||||||
const {bool, func, shape, string} = PropTypes
|
super(props)
|
||||||
const QueryEditor = React.createClass({
|
this.state = {
|
||||||
propTypes: {
|
|
||||||
query: string.isRequired,
|
|
||||||
onUpdate: func.isRequired,
|
|
||||||
config: shape({
|
|
||||||
status: shape({
|
|
||||||
error: string,
|
|
||||||
loading: bool,
|
|
||||||
success: string,
|
|
||||||
warn: string,
|
|
||||||
}),
|
|
||||||
}).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
value: this.props.query,
|
value: this.props.query,
|
||||||
|
isTemplating: false,
|
||||||
|
selectedTemplate: {
|
||||||
|
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
|
||||||
|
},
|
||||||
|
filteredTemplates: this.props.templates,
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
this.handleKeyDown = ::this.handleKeyDown
|
||||||
|
this.handleChange = ::this.handleChange
|
||||||
|
this.handleUpdate = ::this.handleUpdate
|
||||||
|
this.handleChooseTemplate = ::this.handleChooseTemplate
|
||||||
|
this.handleCloseDrawer = ::this.handleCloseDrawer
|
||||||
|
this.findTempVar = ::this.findTempVar
|
||||||
|
this.handleTemplateReplace = ::this.handleTemplateReplace
|
||||||
|
this.handleMouseOverTempVar = ::this.handleMouseOverTempVar
|
||||||
|
this.handleClickTempVar = ::this.handleClickTempVar
|
||||||
|
this.closeDrawer = ::this.closeDrawer
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.query !== nextProps.query) {
|
if (this.props.query !== nextProps.query) {
|
||||||
this.setState({value: nextProps.query})
|
this.setState({value: nextProps.query})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
handleCloseDrawer() {
|
||||||
|
this.setState({isTemplating: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOverTempVar(template) {
|
||||||
|
this.handleTemplateReplace(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickTempVar(template) {
|
||||||
|
// Clicking a tempVar does the same thing as hitting 'Enter'
|
||||||
|
this.handleTemplateReplace(template, 'Enter')
|
||||||
|
this.closeDrawer()
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDrawer() {
|
||||||
|
this.setState({
|
||||||
|
isTemplating: false,
|
||||||
|
selectedTemplate: {
|
||||||
|
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (e.keyCode === ENTER) {
|
const {isTemplating, value} = this.state
|
||||||
|
|
||||||
|
if (isTemplating) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
return this.handleTemplateReplace(this.findTempVar('next'))
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
return this.handleTemplateReplace(this.findTempVar('previous'))
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault()
|
||||||
|
this.handleTemplateReplace(this.state.selectedTemplate, e.key)
|
||||||
|
return this.closeDrawer()
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault()
|
||||||
|
return this.closeDrawer()
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
this.setState({value, isTemplating: false})
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.handleUpdate()
|
this.handleUpdate()
|
||||||
} else if (e.keyCode === ESCAPE) {
|
|
||||||
this.setState({value: this.state.value}, () => {
|
|
||||||
this.editor.blur()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
handleTemplateReplace(selectedTemplate, key) {
|
||||||
|
const {selectionStart, value} = this.editor
|
||||||
|
const isEnter = key === 'Enter'
|
||||||
|
const {tempVar} = selectedTemplate
|
||||||
|
|
||||||
|
let templatedValue
|
||||||
|
const matched = value.match(TEMPLATE_MATCHER)
|
||||||
|
if (matched) {
|
||||||
|
const newTempVar = isEnter
|
||||||
|
? tempVar
|
||||||
|
: tempVar.substring(0, tempVar.length - 1)
|
||||||
|
templatedValue = value.replace(TEMPLATE_MATCHER, newTempVar)
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterModifier = isEnter ? 0 : -1
|
||||||
|
const diffInLength = tempVar.length - matched[0].length + enterModifier
|
||||||
|
|
||||||
|
this.setState({value: templatedValue, selectedTemplate}, () =>
|
||||||
|
this.editor.setSelectionRange(
|
||||||
|
selectionStart + diffInLength,
|
||||||
|
selectionStart + diffInLength
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
findTempVar(direction) {
|
||||||
|
const {filteredTemplates: templates} = this.state
|
||||||
|
const {selectedTemplate} = this.state
|
||||||
|
|
||||||
|
const i = _.findIndex(templates, selectedTemplate)
|
||||||
|
const lastIndex = templates.length - 1
|
||||||
|
|
||||||
|
if (i >= 0) {
|
||||||
|
if (direction === 'next') {
|
||||||
|
return templates[(i + 1) % templates.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 'previous') {
|
||||||
|
if (i === 0) {
|
||||||
|
return templates[lastIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates[i - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates[0]
|
||||||
|
}
|
||||||
|
|
||||||
handleChange() {
|
handleChange() {
|
||||||
this.setState({
|
const {templates} = this.props
|
||||||
value: this.editor.value,
|
const {selectedTemplate} = this.state
|
||||||
})
|
const value = this.editor.value
|
||||||
},
|
const matches = value.match(TEMPLATE_MATCHER)
|
||||||
|
if (matches) {
|
||||||
|
// maintain cursor poition
|
||||||
|
const start = this.editor.selectionStart
|
||||||
|
const end = this.editor.selectionEnd
|
||||||
|
const filteredTemplates = templates.filter(t =>
|
||||||
|
t.tempVar.includes(matches[0].substring(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
const found = filteredTemplates.find(
|
||||||
|
t => t.tempVar === selectedTemplate && selectedTemplate.tempVar
|
||||||
|
)
|
||||||
|
const newTemplate = found ? found : filteredTemplates[0]
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isTemplating: true,
|
||||||
|
selectedTemplate: newTemplate,
|
||||||
|
filteredTemplates,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
this.editor.setSelectionRange(start, end)
|
||||||
|
} else {
|
||||||
|
this.setState({isTemplating: false, value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleUpdate() {
|
handleUpdate() {
|
||||||
this.props.onUpdate(this.state.value)
|
this.props.onUpdate(this.state.value)
|
||||||
},
|
}
|
||||||
|
|
||||||
handleChooseTemplate(template) {
|
handleChooseTemplate(template) {
|
||||||
this.setState({value: template.query})
|
this.setState({value: template.query})
|
||||||
},
|
}
|
||||||
|
|
||||||
|
handleSelectTempVar(tempVar) {
|
||||||
|
this.setState({selectedTemplate: tempVar})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {config: {status}} = this.props
|
const {config: {status}} = this.props
|
||||||
const {value} = this.state
|
const {
|
||||||
|
value,
|
||||||
|
isTemplating,
|
||||||
|
selectedTemplate,
|
||||||
|
filteredTemplates,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-editor">
|
<div className="query-editor">
|
||||||
|
@ -69,13 +198,80 @@ const QueryEditor = React.createClass({
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
onBlur={this.handleUpdate}
|
onBlur={this.handleUpdate}
|
||||||
ref={editor => (this.editor = editor)}
|
ref={editor => this.editor = editor}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
{this.renderStatus(status)}
|
<div
|
||||||
|
className={classNames('varmoji', {'varmoji-rotated': isTemplating})}
|
||||||
|
>
|
||||||
|
<div className="varmoji-container">
|
||||||
|
<div className="varmoji-front">{this.renderStatus(status)}</div>
|
||||||
|
<div className="varmoji-back">
|
||||||
|
{isTemplating
|
||||||
|
? <TemplateDrawer
|
||||||
|
onClickTempVar={this.handleClickTempVar}
|
||||||
|
templates={filteredTemplates}
|
||||||
|
selected={selectedTemplate}
|
||||||
|
onMouseOverTempVar={this.handleMouseOverTempVar}
|
||||||
|
handleClickOutside={this.handleCloseDrawer}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStatus(status) {
|
||||||
|
if (!status) {
|
||||||
|
return (
|
||||||
|
<div className="query-editor--status">
|
||||||
|
<Dropdown
|
||||||
|
items={QUERY_TEMPLATES}
|
||||||
|
selected={'Query Templates'}
|
||||||
|
onChoose={this.handleChooseTemplate}
|
||||||
|
className="query-editor--templates"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.loading) {
|
||||||
|
return (
|
||||||
|
<div className="query-editor--status">
|
||||||
|
<LoadingDots />
|
||||||
|
<Dropdown
|
||||||
|
items={QUERY_TEMPLATES}
|
||||||
|
selected={'Query Templates'}
|
||||||
|
onChoose={this.handleChooseTemplate}
|
||||||
|
className="query-editor--templates"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-editor--status">
|
||||||
|
<span
|
||||||
|
className={classNames('query-status-output', {
|
||||||
|
'query-status-output--error': status.error,
|
||||||
|
'query-status-output--success': status.success,
|
||||||
|
'query-status-output--warning': status.warn,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={classNames('icon', {
|
||||||
|
stop: status.error,
|
||||||
|
checkmark: status.success,
|
||||||
|
'alert-triangle': status.warn,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{status.error || status.warn || status.success}
|
||||||
|
</span>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={QUERY_TEMPLATES}
|
items={QUERY_TEMPLATES}
|
||||||
selected={'Query Templates'}
|
selected={'Query Templates'}
|
||||||
|
@ -84,40 +280,20 @@ const QueryEditor = React.createClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderStatus(status) {
|
const {arrayOf, func, shape, string} = PropTypes
|
||||||
if (!status) {
|
|
||||||
return <div className="query-editor--status" />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.loading) {
|
QueryEditor.propTypes = {
|
||||||
return (
|
query: string.isRequired,
|
||||||
<div className="query-editor--status">
|
onUpdate: func.isRequired,
|
||||||
<LoadingDots />
|
config: shape().isRequired,
|
||||||
</div>
|
templates: arrayOf(
|
||||||
)
|
shape({
|
||||||
}
|
tempVar: string.isRequired,
|
||||||
|
})
|
||||||
return (
|
),
|
||||||
<div
|
}
|
||||||
className={classNames('query-editor--status', {
|
|
||||||
'query-editor--error': status.error,
|
|
||||||
'query-editor--success': status.success,
|
|
||||||
'query-editor--warning': status.warn,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames('icon', {
|
|
||||||
stop: status.error,
|
|
||||||
checkmark: status.success,
|
|
||||||
'alert-triangle': status.warn,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{status.error || status.warn || status.success}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default QueryEditor
|
export default QueryEditor
|
||||||
|
|
|
@ -4,14 +4,7 @@ import QueryBuilder from './QueryBuilder'
|
||||||
import QueryMakerTab from './QueryMakerTab'
|
import QueryMakerTab from './QueryMakerTab'
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import buildInfluxQLQuery from 'utils/influxql'
|
||||||
|
|
||||||
const {
|
const {arrayOf, func, node, number, shape, string} = PropTypes
|
||||||
arrayOf,
|
|
||||||
func,
|
|
||||||
node,
|
|
||||||
number,
|
|
||||||
shape,
|
|
||||||
string,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
const QueryMaker = React.createClass({
|
const QueryMaker = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -25,6 +18,11 @@ const QueryMaker = React.createClass({
|
||||||
upper: string,
|
upper: string,
|
||||||
lower: string,
|
lower: string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
actions: shape({
|
actions: shape({
|
||||||
chooseNamespace: func.isRequired,
|
chooseNamespace: func.isRequired,
|
||||||
chooseMeasurement: func.isRequired,
|
chooseMeasurement: func.isRequired,
|
||||||
|
@ -76,7 +74,7 @@ const QueryMaker = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderQueryBuilder() {
|
renderQueryBuilder() {
|
||||||
const {timeRange, actions, source} = this.props
|
const {timeRange, actions, source, templates} = this.props
|
||||||
const query = this.getActiveQuery()
|
const query = this.getActiveQuery()
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
@ -93,6 +91,7 @@ const QueryMaker = React.createClass({
|
||||||
<QueryBuilder
|
<QueryBuilder
|
||||||
source={source}
|
source={source}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
|
templates={templates}
|
||||||
query={query}
|
query={query}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
onAddQuery={this.handleAddQuery}
|
onAddQuery={this.handleAddQuery}
|
||||||
|
|
|
@ -85,8 +85,7 @@ const Visualization = React.createClass({
|
||||||
editQueryStatus,
|
editQueryStatus,
|
||||||
activeQueryIndex,
|
activeQueryIndex,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {source} = this.context
|
const {source: {links: {proxy}}} = this.context
|
||||||
const proxyLink = source.links.proxy
|
|
||||||
const {view} = this.state
|
const {view} = this.state
|
||||||
|
|
||||||
const statements = queryConfigs.map(query => {
|
const statements = queryConfigs.map(query => {
|
||||||
|
@ -94,7 +93,7 @@ const Visualization = React.createClass({
|
||||||
return {text, id: query.id}
|
return {text, id: query.id}
|
||||||
})
|
})
|
||||||
const queries = statements.filter(s => s.text !== null).map(s => {
|
const queries = statements.filter(s => s.text !== null).map(s => {
|
||||||
return {host: [proxyLink], text: s.text, id: s.id}
|
return {host: [proxy], text: s.text, id: s.id}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -43,7 +43,9 @@ const Header = React.createClass({
|
||||||
<div className="page-header full-width-no-scrollbar">
|
<div className="page-header full-width-no-scrollbar">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>Data Explorer</h1>
|
<h1 className="page-header__title">
|
||||||
|
Data Explorer
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-header__right">
|
<div className="page-header__right">
|
||||||
<GraphTips />
|
<GraphTips />
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const HostsPage = React.createClass({
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>
|
<h1 className="page-header__title">
|
||||||
Host List
|
Host List
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ class KapacitorForm extends Component {
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>
|
<h1 className="page-header__title">
|
||||||
Configure Kapacitor
|
Configure Kapacitor
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,7 +50,7 @@ const PageContents = ({children, source}) => (
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>Kapacitor Rules</h1>
|
<h1 className="page-header__title">Kapacitor Rules</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-header__right">
|
<div className="page-header__right">
|
||||||
<SourceIndicator sourceName={source && source.name} />
|
<SourceIndicator sourceName={source && source.name} />
|
||||||
|
|
|
@ -14,7 +14,9 @@ export const handleSuccess = (data, query, editQueryStatus) => {
|
||||||
const series = _.get(results, ['0', 'series'], false)
|
const series = _.get(results, ['0', 'series'], false)
|
||||||
// 200 from server and no results = warn
|
// 200 from server and no results = warn
|
||||||
if (!series && !error) {
|
if (!series && !error) {
|
||||||
editQueryStatus(query.id, {warn: 'Your query is syntactically correct but returned no results'})
|
editQueryStatus(query.id, {
|
||||||
|
warn: 'Your query is syntactically correct but returned no results',
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +39,19 @@ export const handleError = (error, query, editQueryStatus) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchTimeSeriesAsync = async ({source, db, rp, query}, editQueryStatus = noop) => {
|
export const fetchTimeSeriesAsync = async (
|
||||||
|
{source, db, rp, query, tempVars},
|
||||||
|
editQueryStatus = noop
|
||||||
|
) => {
|
||||||
handleLoading(query, editQueryStatus)
|
handleLoading(query, editQueryStatus)
|
||||||
try {
|
try {
|
||||||
const {data} = await proxy({source, db, rp, query: query.text})
|
const {data} = await proxy({
|
||||||
|
source,
|
||||||
|
db,
|
||||||
|
rp,
|
||||||
|
query: query.text,
|
||||||
|
tempVars,
|
||||||
|
})
|
||||||
return handleSuccess(data, query, editQueryStatus)
|
return handleSuccess(data, query, editQueryStatus)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorThrown(error)
|
errorThrown(error)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import AJAX from 'utils/ajax'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
||||||
|
|
||||||
export const showDatabases = async (source) => {
|
export const showDatabases = async source => {
|
||||||
const query = 'SHOW DATABASES'
|
const query = 'SHOW DATABASES'
|
||||||
return await proxy({source, query})
|
return await proxy({source, query})
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export const showDatabases = async (source) => {
|
||||||
export const showRetentionPolicies = async (source, databases) => {
|
export const showRetentionPolicies = async (source, databases) => {
|
||||||
let query
|
let query
|
||||||
if (Array.isArray(databases)) {
|
if (Array.isArray(databases)) {
|
||||||
query = databases.map((db) => `SHOW RETENTION POLICIES ON "${db}"`).join(';')
|
query = databases.map(db => `SHOW RETENTION POLICIES ON "${db}"`).join(';')
|
||||||
} else {
|
} else {
|
||||||
query = `SHOW RETENTION POLICIES ON "${databases}"`
|
query = `SHOW RETENTION POLICIES ON "${databases}"`
|
||||||
}
|
}
|
||||||
|
@ -30,24 +30,35 @@ export function killQuery(source, queryId) {
|
||||||
return proxy({source, query})
|
return proxy({source, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showMeasurements(source, db) {
|
export const showMeasurements = async (source, db) => {
|
||||||
const query = 'SHOW MEASUREMENTS'
|
const query = 'SHOW MEASUREMENTS'
|
||||||
|
|
||||||
return proxy({source, db, query})
|
return await proxy({source, db, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTagKeys({source, database, retentionPolicy, measurement}) {
|
export const showTagKeys = async ({
|
||||||
|
source,
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
measurement,
|
||||||
|
}) => {
|
||||||
const rp = _.toString(retentionPolicy)
|
const rp = _.toString(retentionPolicy)
|
||||||
const query = `SHOW TAG KEYS FROM "${rp}"."${measurement}"`
|
const query = `SHOW TAG KEYS FROM "${rp}"."${measurement}"`
|
||||||
return proxy({source, db: database, rp: retentionPolicy, query})
|
return await proxy({source, db: database, rp: retentionPolicy, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTagValues({source, database, retentionPolicy, measurement, tagKeys}) {
|
export const showTagValues = async ({
|
||||||
const keys = tagKeys.sort().map((k) => `"${k}"`).join(', ')
|
source,
|
||||||
|
database,
|
||||||
|
retentionPolicy,
|
||||||
|
measurement,
|
||||||
|
tagKeys,
|
||||||
|
}) => {
|
||||||
|
const keys = tagKeys.sort().map(k => `"${k}"`).join(', ')
|
||||||
const rp = _.toString(retentionPolicy)
|
const rp = _.toString(retentionPolicy)
|
||||||
const query = `SHOW TAG VALUES FROM "${rp}"."${measurement}" WITH KEY IN (${keys})`
|
const query = `SHOW TAG VALUES FROM "${rp}"."${measurement}" WITH KEY IN (${keys})`
|
||||||
|
|
||||||
return proxy({source, db: database, rp: retentionPolicy, query})
|
return await proxy({source, db: database, rp: retentionPolicy, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showShards() {
|
export function showShards() {
|
||||||
|
@ -56,7 +67,14 @@ export function showShards() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRetentionPolicy({host, database, rpName, duration, replicationFactor, clusterID}) {
|
export function createRetentionPolicy({
|
||||||
|
host,
|
||||||
|
database,
|
||||||
|
rpName,
|
||||||
|
duration,
|
||||||
|
replicationFactor,
|
||||||
|
clusterID,
|
||||||
|
}) {
|
||||||
const statement = `CREATE RETENTION POLICY "${rpName}" ON "${database}" DURATION ${duration} REPLICATION ${replicationFactor}`
|
const statement = `CREATE RETENTION POLICY "${rpName}" ON "${database}" DURATION ${duration} REPLICATION ${replicationFactor}`
|
||||||
const url = buildInfluxUrl({host, statement})
|
const url = buildInfluxUrl({host, statement})
|
||||||
|
|
||||||
|
@ -70,8 +88,8 @@ export function dropShard(host, shard, clusterID) {
|
||||||
return proxy(url, clusterID)
|
return proxy(url, clusterID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showFieldKeys(source, db, measurement, rp) {
|
export const showFieldKeys = async (source, db, measurement, rp) => {
|
||||||
const query = `SHOW FIELD KEYS FROM "${rp}"."${measurement}"`
|
const query = `SHOW FIELD KEYS FROM "${rp}"."${measurement}"`
|
||||||
|
|
||||||
return proxy({source, query, db})
|
return await proxy({source, query, db})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import {proxy} from 'utils/queryUrlGenerator'
|
|
||||||
|
|
||||||
const fetchTimeSeries = async (source, database, query) => {
|
|
||||||
try {
|
|
||||||
return await proxy({source, query, database})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('error from proxy: ', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchTimeSeries
|
|
|
@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
arrayOf,
|
arrayOf,
|
||||||
|
bool,
|
||||||
element,
|
element,
|
||||||
func,
|
func,
|
||||||
number,
|
number,
|
||||||
|
@ -12,82 +13,141 @@ const {
|
||||||
string,
|
string,
|
||||||
} = PropTypes
|
} = PropTypes
|
||||||
|
|
||||||
const AutoRefresh = (ComposedComponent) => {
|
const AutoRefresh = ComposedComponent => {
|
||||||
const wrapper = React.createClass({
|
const wrapper = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: element,
|
children: element,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
queries: arrayOf(shape({
|
templates: arrayOf(
|
||||||
host: oneOfType([string, arrayOf(string)]),
|
shape({
|
||||||
text: string,
|
type: string.isRequired,
|
||||||
}).isRequired).isRequired,
|
label: string.isRequired,
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
query: shape({
|
||||||
|
db: string,
|
||||||
|
rp: string,
|
||||||
|
influxql: string,
|
||||||
|
}),
|
||||||
|
values: arrayOf(
|
||||||
|
shape({
|
||||||
|
type: string.isRequired,
|
||||||
|
value: string.isRequired,
|
||||||
|
selected: bool,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
queries: arrayOf(
|
||||||
|
shape({
|
||||||
|
host: oneOfType([string, arrayOf(string)]),
|
||||||
|
text: string,
|
||||||
|
}).isRequired
|
||||||
|
).isRequired,
|
||||||
editQueryStatus: func,
|
editQueryStatus: func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
lastQuerySuccessful: false,
|
lastQuerySuccessful: false,
|
||||||
timeSeries: [],
|
timeSeries: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {queries, autoRefresh} = this.props
|
const {queries, autoRefresh} = this.props
|
||||||
this.executeQueries(queries)
|
this.executeQueries(queries)
|
||||||
if (autoRefresh) {
|
if (autoRefresh) {
|
||||||
this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh)
|
this.intervalID = setInterval(
|
||||||
|
() => this.executeQueries(queries),
|
||||||
|
autoRefresh
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const shouldRefetch = this.queryDifference(this.props.queries, nextProps.queries).length
|
const queriesDidUpdate = this.queryDifference(
|
||||||
|
this.props.queries,
|
||||||
|
nextProps.queries
|
||||||
|
).length
|
||||||
|
|
||||||
|
const tempVarsDidUpdate = !_.isEqual(
|
||||||
|
this.props.templates,
|
||||||
|
nextProps.templates
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldRefetch = queriesDidUpdate || tempVarsDidUpdate
|
||||||
|
|
||||||
if (shouldRefetch) {
|
if (shouldRefetch) {
|
||||||
this.executeQueries(nextProps.queries)
|
this.executeQueries(nextProps.queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.props.autoRefresh !== nextProps.autoRefresh) || shouldRefetch) {
|
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
|
||||||
clearInterval(this.intervalID)
|
clearInterval(this.intervalID)
|
||||||
|
|
||||||
if (nextProps.autoRefresh) {
|
if (nextProps.autoRefresh) {
|
||||||
this.intervalID = setInterval(() => this.executeQueries(nextProps.queries), nextProps.autoRefresh)
|
this.intervalID = setInterval(
|
||||||
|
() => this.executeQueries(nextProps.queries),
|
||||||
|
nextProps.autoRefresh
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
queryDifference(left, right) {
|
queryDifference(left, right) {
|
||||||
const leftStrs = left.map((q) => `${q.host}${q.text}`)
|
const leftStrs = left.map(q => `${q.host}${q.text}`)
|
||||||
const rightStrs = right.map((q) => `${q.host}${q.text}`)
|
const rightStrs = right.map(q => `${q.host}${q.text}`)
|
||||||
return _.difference(_.union(leftStrs, rightStrs), _.intersection(leftStrs, rightStrs))
|
return _.difference(
|
||||||
|
_.union(leftStrs, rightStrs),
|
||||||
|
_.intersection(leftStrs, rightStrs)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async executeQueries(queries) {
|
|
||||||
|
executeQueries(queries) {
|
||||||
|
const {templates = [], editQueryStatus} = this.props
|
||||||
|
|
||||||
if (!queries.length) {
|
if (!queries.length) {
|
||||||
this.setState({
|
this.setState({timeSeries: []})
|
||||||
timeSeries: [],
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({isFetching: true})
|
this.setState({isFetching: true})
|
||||||
let count = 0
|
|
||||||
const newSeries = []
|
const selectedTempVarTemplates = templates.map(template => {
|
||||||
for (const query of queries) {
|
const selectedValues = template.values.filter(value => value.selected)
|
||||||
|
return {...template, values: selectedValues}
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeSeriesPromises = queries.map(query => {
|
||||||
const {host, database, rp} = query
|
const {host, database, rp} = query
|
||||||
// TODO: enact this via an action creator so redux will know about it; currently errors are used as responses here
|
return fetchTimeSeriesAsync(
|
||||||
// TODO: may need to make this a try/catch
|
{
|
||||||
const response = await fetchTimeSeriesAsync({source: host, db: database, rp, query}, this.props.editQueryStatus)
|
source: host,
|
||||||
newSeries.push({response})
|
db: database,
|
||||||
count += 1
|
rp,
|
||||||
if (count === queries.length) {
|
query,
|
||||||
const querySuccessful = !this._noResultsForQuery(newSeries)
|
tempVars: selectedTempVarTemplates,
|
||||||
this.setState({
|
},
|
||||||
lastQuerySuccessful: querySuccessful,
|
editQueryStatus
|
||||||
isFetching: false,
|
)
|
||||||
timeSeries: newSeries,
|
})
|
||||||
})
|
|
||||||
}
|
Promise.all(timeSeriesPromises).then(timeSeries => {
|
||||||
}
|
const newSeries = timeSeries.map(response => ({response}))
|
||||||
|
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
timeSeries: newSeries,
|
||||||
|
lastQuerySuccessful,
|
||||||
|
isFetching: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.intervalID)
|
clearInterval(this.intervalID)
|
||||||
this.intervalID = false
|
this.intervalID = false
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {timeSeries} = this.state
|
const {timeSeries} = this.state
|
||||||
|
|
||||||
|
@ -95,16 +155,14 @@ const AutoRefresh = (ComposedComponent) => {
|
||||||
return this.renderFetching(timeSeries)
|
return this.renderFetching(timeSeries)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._noResultsForQuery(timeSeries) || !this.state.lastQuerySuccessful) {
|
if (
|
||||||
|
this._noResultsForQuery(timeSeries) ||
|
||||||
|
!this.state.lastQuerySuccessful
|
||||||
|
) {
|
||||||
return this.renderNoResults()
|
return this.renderNoResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ComposedComponent {...this.props} data={timeSeries} />
|
||||||
<ComposedComponent
|
|
||||||
{...this.props}
|
|
||||||
data={timeSeries}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,8 +198,8 @@ const AutoRefresh = (ComposedComponent) => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.every((datum) => {
|
return data.every(datum => {
|
||||||
return datum.response.results.every((result) => {
|
return datum.response.results.every(result => {
|
||||||
return Object.keys(result).length === 0
|
return Object.keys(result).length === 0
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,10 @@ import OnClickOutside from 'shared/components/OnClickOutside'
|
||||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||||
|
|
||||||
const DeleteButton = ({onClickDelete}) => (
|
const DeleteButton = ({onClickDelete}) => (
|
||||||
<button className="btn btn-xs btn-danger admin-table--hidden" onClick={onClickDelete}>
|
<button
|
||||||
|
className="btn btn-xs btn-danger admin-table--hidden"
|
||||||
|
onClick={onClickDelete}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
@ -35,23 +38,24 @@ class DeleteConfirmButtons extends Component {
|
||||||
const {onDelete, item} = this.props
|
const {onDelete, item} = this.props
|
||||||
const {isConfirming} = this.state
|
const {isConfirming} = this.state
|
||||||
|
|
||||||
return isConfirming ?
|
return isConfirming
|
||||||
<ConfirmButtons onConfirm={onDelete} item={item} onCancel={this.handleCancel} /> :
|
? <ConfirmButtons
|
||||||
<DeleteButton onClickDelete={this.handleClickDelete} />
|
onConfirm={onDelete}
|
||||||
|
item={item}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
/>
|
||||||
|
: <DeleteButton onClickDelete={this.handleClickDelete} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {func, oneOfType, shape, string} = PropTypes
|
||||||
func,
|
|
||||||
shape,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
DeleteButton.propTypes = {
|
DeleteButton.propTypes = {
|
||||||
onClickDelete: func.isRequired,
|
onClickDelete: func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteConfirmButtons.propTypes = {
|
DeleteConfirmButtons.propTypes = {
|
||||||
item: shape({}),
|
item: oneOfType([(string, shape())]),
|
||||||
onDelete: func.isRequired,
|
onDelete: func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React, {Component, PropTypes} from 'react'
|
import React, {Component, PropTypes} from 'react'
|
||||||
import {Link} from 'react-router'
|
import {Link} from 'react-router'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||||
|
|
||||||
class Dropdown extends Component {
|
class Dropdown extends Component {
|
||||||
|
@ -134,6 +133,7 @@ Dropdown.propTypes = {
|
||||||
})
|
})
|
||||||
).isRequired,
|
).isRequired,
|
||||||
onChoose: func.isRequired,
|
onChoose: func.isRequired,
|
||||||
|
onClick: func,
|
||||||
addNew: shape({
|
addNew: shape({
|
||||||
url: string.isRequired,
|
url: string.isRequired,
|
||||||
text: string.isRequired,
|
text: string.isRequired,
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const LayoutRenderer = React.createClass({
|
||||||
type: string.isRequired,
|
type: string.isRequired,
|
||||||
}).isRequired
|
}).isRequired
|
||||||
),
|
),
|
||||||
|
templates: arrayOf(shape()).isRequired,
|
||||||
host: string,
|
host: string,
|
||||||
source: string,
|
source: string,
|
||||||
onPositionChange: func,
|
onPositionChange: func,
|
||||||
|
@ -89,12 +90,41 @@ export const LayoutRenderer = React.createClass({
|
||||||
return text
|
return text
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderRefreshingGraph(type, queries) {
|
||||||
|
const {autoRefresh, templates} = this.props
|
||||||
|
|
||||||
|
if (type === 'single-stat') {
|
||||||
|
return (
|
||||||
|
<RefreshingSingleStat
|
||||||
|
queries={[queries[0]]}
|
||||||
|
templates={templates}
|
||||||
|
autoRefresh={autoRefresh}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayOptions = {
|
||||||
|
stepPlot: type === 'line-stepplot',
|
||||||
|
stackedGraph: type === 'line-stacked',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RefreshingLineGraph
|
||||||
|
queries={queries}
|
||||||
|
templates={templates}
|
||||||
|
autoRefresh={autoRefresh}
|
||||||
|
showSingleStat={type === 'line-plus-single-stat'}
|
||||||
|
displayOptions={displayOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
generateVisualizations() {
|
generateVisualizations() {
|
||||||
const {autoRefresh, timeRange, source, cells, onEditCell, onRenameCell, onUpdateCell, onDeleteCell, onSummonOverlayTechnologies, shouldNotBeEditable} = this.props
|
const {timeRange, source, cells, onEditCell, onRenameCell, onUpdateCell, onDeleteCell, onSummonOverlayTechnologies, shouldNotBeEditable} = this.props
|
||||||
|
|
||||||
return cells.map((cell) => {
|
return cells.map((cell) => {
|
||||||
const qs = cell.queries.map((query) => {
|
const queries = cell.queries.map((query) => {
|
||||||
// TODO: Canned dashboards use an old query schema,
|
// TODO: Canned dashboards (and possibly Kubernetes dashboard) use an old query schema,
|
||||||
// which does not have enough information for the new `buildInfluxQLQuery` function
|
// which does not have enough information for the new `buildInfluxQLQuery` function
|
||||||
// to operate on. We will use `buildQueryForOldQuerySchema` until we conform
|
// to operate on. We will use `buildQueryForOldQuerySchema` until we conform
|
||||||
// on a stable query representation.
|
// on a stable query representation.
|
||||||
|
@ -112,29 +142,6 @@ export const LayoutRenderer = React.createClass({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cell.type === 'single-stat') {
|
|
||||||
return (
|
|
||||||
<div key={cell.i}>
|
|
||||||
<NameableGraph
|
|
||||||
onEditCell={onEditCell}
|
|
||||||
onRenameCell={onRenameCell}
|
|
||||||
onUpdateCell={onUpdateCell}
|
|
||||||
onDeleteCell={onDeleteCell}
|
|
||||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
|
||||||
shouldNotBeEditable={shouldNotBeEditable}
|
|
||||||
cell={cell}
|
|
||||||
>
|
|
||||||
<RefreshingSingleStat queries={[qs[0]]} autoRefresh={autoRefresh} />
|
|
||||||
</NameableGraph>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayOptions = {
|
|
||||||
stepPlot: cell.type === 'line-stepplot',
|
|
||||||
stackedGraph: cell.type === 'line-stacked',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={cell.i}>
|
<div key={cell.i}>
|
||||||
<NameableGraph
|
<NameableGraph
|
||||||
|
@ -146,12 +153,7 @@ export const LayoutRenderer = React.createClass({
|
||||||
shouldNotBeEditable={shouldNotBeEditable}
|
shouldNotBeEditable={shouldNotBeEditable}
|
||||||
cell={cell}
|
cell={cell}
|
||||||
>
|
>
|
||||||
<RefreshingLineGraph
|
{this.renderRefreshingGraph(cell.type, queries)}
|
||||||
queries={qs}
|
|
||||||
autoRefresh={autoRefresh}
|
|
||||||
showSingleStat={cell.type === 'line-plus-single-stat'}
|
|
||||||
displayOptions={displayOptions}
|
|
||||||
/>
|
|
||||||
</NameableGraph>
|
</NameableGraph>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
|
const OverlayTechnologies = ({children}) => <div className="overlay-technology">{children}</div>
|
||||||
|
|
||||||
|
const {
|
||||||
|
node,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
OverlayTechnologies.propTypes = {
|
||||||
|
children: node.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverlayTechnologies
|
|
@ -1,59 +0,0 @@
|
||||||
import React, {PropTypes} from 'react'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
|
||||||
|
|
||||||
const Dropdown = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
})).isRequired,
|
|
||||||
onChoose: PropTypes.func.isRequired,
|
|
||||||
className: PropTypes.string,
|
|
||||||
},
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
isOpen: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleClickOutside() {
|
|
||||||
this.setState({isOpen: false})
|
|
||||||
},
|
|
||||||
handleSelection(item) {
|
|
||||||
this.toggleMenu()
|
|
||||||
this.props.onChoose(item)
|
|
||||||
},
|
|
||||||
toggleMenu(e) {
|
|
||||||
if (e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
this.setState({isOpen: !this.state.isOpen})
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const self = this
|
|
||||||
const {items, className} = self.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div onClick={this.toggleMenu} className={classNames(`dropdown ${className}`, {open: self.state.isOpen})}>
|
|
||||||
<div className="btn btn-sm btn-info dropdown-toggle">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
{self.state.isOpen ?
|
|
||||||
<ul className="dropdown-menu show">
|
|
||||||
{items.map((item, i) => {
|
|
||||||
return (
|
|
||||||
<li className="dropdown-item" key={i} onClick={() => self.handleSelection(item)}>
|
|
||||||
<a href="#">
|
|
||||||
{item.text}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default OnClickOutside(Dropdown)
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import OnClickOutside from 'react-onclickoutside'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
const TemplateDrawer = ({
|
||||||
|
templates,
|
||||||
|
selected,
|
||||||
|
onMouseOverTempVar,
|
||||||
|
onClickTempVar,
|
||||||
|
}) => (
|
||||||
|
<div className="template-drawer">
|
||||||
|
{templates.map(t => (
|
||||||
|
<div className={classNames('template-drawer--item', {'template-drawer--selected': t.tempVar === selected.tempVar})}
|
||||||
|
onMouseOver={() => {
|
||||||
|
onMouseOverTempVar(t)
|
||||||
|
}}
|
||||||
|
onClick={() => onClickTempVar(t)}
|
||||||
|
key={t.tempVar}
|
||||||
|
>
|
||||||
|
{' '}{t.tempVar}{' '}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const {arrayOf, func, shape, string} = PropTypes
|
||||||
|
|
||||||
|
TemplateDrawer.propTypes = {
|
||||||
|
templates: arrayOf(
|
||||||
|
shape({
|
||||||
|
tempVar: string.isRequired,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selected: shape({
|
||||||
|
tempVar: string,
|
||||||
|
}),
|
||||||
|
onMouseOverTempVar: func.isRequired,
|
||||||
|
onClickTempVar: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnClickOutside(TemplateDrawer)
|
|
@ -0,0 +1 @@
|
||||||
|
export const TEMPLATE_VARIABLE_SELECTED = 'TEMPLATE_VARIABLE_SELECTED'
|
|
@ -384,7 +384,7 @@ export const HEARTBEAT_INTERVAL = 10000 // ms
|
||||||
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
|
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
|
||||||
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
||||||
|
|
||||||
export const SHORT_NOTIFICATION_DISMISS_DELAY = 1500 // in milliseconds
|
export const SHORT_NOTIFICATION_DISMISS_DELAY = 2000 // in milliseconds
|
||||||
|
|
||||||
export const REVERT_STATE_DELAY = 1500 // ms
|
export const REVERT_STATE_DELAY = 1500 // ms
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import databases from 'shared/parsing/showDatabases'
|
||||||
|
import measurements from 'shared/parsing/showMeasurements'
|
||||||
|
import fieldKeys from 'shared/parsing/showFieldKeys'
|
||||||
|
import tagKeys from 'shared/parsing/showTagKeys'
|
||||||
|
import tagValues from 'shared/parsing/showTagValues'
|
||||||
|
|
||||||
|
const parsers = {
|
||||||
|
databases,
|
||||||
|
measurements: data => {
|
||||||
|
const {errors, measurementSets} = measurements(data)
|
||||||
|
return {errors, measurements: measurementSets[0].measurements}
|
||||||
|
},
|
||||||
|
fieldKeys: (data, key) => {
|
||||||
|
const {errors, fieldSets} = fieldKeys(data)
|
||||||
|
return {errors, fieldKeys: fieldSets[key]}
|
||||||
|
},
|
||||||
|
tagKeys,
|
||||||
|
tagValues: (data, key) => {
|
||||||
|
const {errors, tags} = tagValues(data)
|
||||||
|
return {errors, tagValues: tags[key]}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parsers
|
|
@ -2,7 +2,7 @@ export default function parseShowFieldKeys(response) {
|
||||||
const errors = []
|
const errors = []
|
||||||
const fieldSets = {}
|
const fieldSets = {}
|
||||||
|
|
||||||
response.results.forEach((result) => {
|
response.results.forEach(result => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
errors.push(result.error)
|
errors.push(result.error)
|
||||||
return
|
return
|
||||||
|
@ -14,7 +14,7 @@ export default function parseShowFieldKeys(response) {
|
||||||
|
|
||||||
const series = result.series[0]
|
const series = result.series[0]
|
||||||
const fieldKeyIndex = series.columns.indexOf('fieldKey')
|
const fieldKeyIndex = series.columns.indexOf('fieldKey')
|
||||||
const fields = series.values.map((value) => {
|
const fields = series.values.map(value => {
|
||||||
return value[fieldKeyIndex]
|
return value[fieldKeyIndex]
|
||||||
})
|
})
|
||||||
const measurement = series.name
|
const measurement = series.name
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function parseShowMeasurements(response) {
|
||||||
|
|
||||||
const series = result.series[0]
|
const series = result.series[0]
|
||||||
const measurementNameIndex = series.columns.indexOf('name')
|
const measurementNameIndex = series.columns.indexOf('name')
|
||||||
const measurements = series.values.map((value) => value[measurementNameIndex])
|
const measurements = series.values.map(value => value[measurementNameIndex])
|
||||||
|
|
||||||
measurementSets.push({
|
measurementSets.push({
|
||||||
index,
|
index,
|
||||||
|
@ -34,4 +34,3 @@ export default function parseShowMeasurements(response) {
|
||||||
measurementSets,
|
measurementSets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ManageSources extends Component {
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>Configuration</h1>
|
<h1 className="page-header__title">Configuration</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,7 +115,7 @@ export const SourcePage = React.createClass({
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-header__container">
|
<div className="page-header__container">
|
||||||
<div className="page-header__left">
|
<div className="page-header__left">
|
||||||
<h1>
|
<h1 className="page-header__title">
|
||||||
{editMode ? 'Edit Source' : 'Add a New Source'}
|
{editMode ? 'Edit Source' : 'Add a New Source'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,19 +11,10 @@
|
||||||
border-radius: 0 $radius 0 0;
|
border-radius: 0 $radius 0 0;
|
||||||
background-color: $query-editor--bg;
|
background-color: $query-editor--bg;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
z-index: 2; /* Minimum amount to obcure the toggle flip within Query Builder. Will fix later */
|
||||||
.query-editor--field,
|
|
||||||
.query-editor--status {
|
|
||||||
font-family: $code-font;
|
|
||||||
transition:
|
|
||||||
color 0.25s ease,
|
|
||||||
background-color 0.25s ease,
|
|
||||||
border-color 0.25s ease;
|
|
||||||
border: 2px solid $query-editor--bg;
|
|
||||||
background-color: $query-editor--field-bg;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.query-editor--field {
|
.query-editor--field {
|
||||||
|
font-family: $code-font;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -34,7 +25,13 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $query-editor--field-height;
|
height: $query-editor--field-height;
|
||||||
|
transition:
|
||||||
|
color 0.25s ease,
|
||||||
|
background-color 0.25s ease,
|
||||||
|
border-color 0.25s ease;
|
||||||
|
border: 2px solid $query-editor--bg;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
background-color: $query-editor--field-bg;
|
||||||
color: $query-editor--field-text;
|
color: $query-editor--field-text;
|
||||||
padding: 12px 10px 0 10px;
|
padding: 12px 10px 0 10px;
|
||||||
border-radius: $radius $radius 0 0;
|
border-radius: $radius $radius 0 0;
|
||||||
|
@ -49,33 +46,16 @@
|
||||||
color: $query-editor--field-text !important;
|
color: $query-editor--field-text !important;
|
||||||
border-color: $c-pool;
|
border-color: $c-pool;
|
||||||
}
|
}
|
||||||
&:focus + .query-editor--status {
|
&:focus + .varmoji {
|
||||||
border-color: $c-pool;
|
border-color: $c-pool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.query-editor--status {
|
.query-editor--status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
height: $query-editor--status-height;
|
height: $query-editor--status-height;
|
||||||
line-height: $query-editor--status-height;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 0 10px;
|
|
||||||
padding-right: ($query-editor--templates-width + ($query-editor--templates-offset * 2)) !important;
|
|
||||||
border-radius: 0 0 $radius $radius;
|
|
||||||
border-top: 0;
|
|
||||||
color: $query-editor--status-default;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
span.icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error State */
|
|
||||||
&.query-editor--error { color: $query-editor--status-error; }
|
|
||||||
/* Warning State */
|
|
||||||
&.query-editor--warning { color: $query-editor--status-warning; }
|
|
||||||
/* Success State */
|
|
||||||
&.query-editor--success { color: $query-editor--status-success; }
|
|
||||||
/* Loading State */
|
/* Loading State */
|
||||||
.loading-dots {
|
.loading-dots {
|
||||||
bottom: $query-editor--templates-offset;
|
bottom: $query-editor--templates-offset;
|
||||||
|
@ -83,10 +63,30 @@
|
||||||
transform: translateY(50%);
|
transform: translateY(50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.query-status-output {
|
||||||
|
flex: 1 0 0;
|
||||||
|
display: inline-block;
|
||||||
|
color: $query-editor--status-default;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 10px;
|
||||||
|
line-height: $query-editor--status-height;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: $code-font;
|
||||||
|
|
||||||
|
span.icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
/* Error State */
|
||||||
|
&.query-status-output--error { color: $query-editor--status-error; }
|
||||||
|
/* Warning State */
|
||||||
|
&.query-status-output--warning { color: $query-editor--status-warning; }
|
||||||
|
/* Success State */
|
||||||
|
&.query-status-output--success { color: $query-editor--status-success; }
|
||||||
|
}
|
||||||
.dropdown.query-editor--templates {
|
.dropdown.query-editor--templates {
|
||||||
position: absolute;
|
margin: 0 4px 0 0 ;
|
||||||
bottom: ($query-editor--templates-offset - 8px);
|
|
||||||
right: $query-editor--templates-offset;
|
|
||||||
|
|
||||||
div.dropdown-toggle.btn.btn-sm {
|
div.dropdown-toggle.btn.btn-sm {
|
||||||
width: $query-editor--templates-width;
|
width: $query-editor--templates-width;
|
||||||
|
@ -103,6 +103,88 @@
|
||||||
min-width: $query-editor--templates-menu-width;
|
min-width: $query-editor--templates-menu-width;
|
||||||
max-width: $query-editor--templates-menu-width;
|
max-width: $query-editor--templates-menu-width;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Varmoji Flipper
|
||||||
|
-------------------------------------
|
||||||
|
Handles the 3D flip transition between two states (isTemplating)
|
||||||
|
Contents could in theory be anything
|
||||||
|
*/
|
||||||
|
.varmoji {
|
||||||
|
transition: border-color 0.25s ease;
|
||||||
|
border: 2px solid $query-editor--bg;
|
||||||
|
border-top: 0;
|
||||||
|
background-color: $query-editor--field-bg;
|
||||||
|
border-radius: 0 0 $radius $radius;
|
||||||
|
height: $query-editor--status-height;
|
||||||
|
width: 100%;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
.varmoji-container {
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
position: relative;
|
||||||
|
transform-origin: 100% #{$query-editor--status-height / 2};
|
||||||
|
}
|
||||||
|
.varmoji-front,
|
||||||
|
.varmoji-back {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: $query-editor--status-height;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.varmoji-front {
|
||||||
|
z-index: 3;
|
||||||
|
transform: rotateX(0deg);
|
||||||
|
}
|
||||||
|
.varmoji-back {
|
||||||
|
z-index: 2;
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
.varmoji.varmoji-rotated .varmoji-container {
|
||||||
|
transform: rotateX(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Template Drawer
|
||||||
|
-------------------------------------
|
||||||
|
Not sure if this needs its own stylesheet
|
||||||
|
*/
|
||||||
|
|
||||||
|
.template-drawer {
|
||||||
|
height: $query-editor--status-height;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.template-drawer--item {
|
||||||
|
margin-right: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $code-font;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
height: ($query-editor--status-height - 14px);
|
||||||
|
line-height: ($query-editor--status-height - 14px);
|
||||||
|
padding: 0 6px;
|
||||||
|
background-color: $query-editor--field-bg;
|
||||||
|
color: $c-comet;
|
||||||
|
border-radius: $radius-small;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.25s ease,
|
||||||
|
background-color 0.25s ease;
|
||||||
|
|
||||||
|
/* Selected State */
|
||||||
|
&.template-drawer--selected {
|
||||||
|
color: $g20-white;
|
||||||
|
background-color: $c-star;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%);
|
background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,45 @@
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// table-custom class allows us to make a table from divs so we can use
|
||||||
|
// forms inside a table
|
||||||
|
.table-custom {
|
||||||
|
display: table !important;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 2px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
.thead {
|
||||||
|
display: table-header-group;
|
||||||
|
color: white;
|
||||||
|
color: $g17-whisper !important;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
.th {
|
||||||
|
display: table-cell;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $g14-chromium !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 6px 8px !important;
|
||||||
|
}
|
||||||
|
.tbody {
|
||||||
|
display: table-row-group;
|
||||||
|
}
|
||||||
|
.tr {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
.td {
|
||||||
|
display: table-cell;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $g14-chromium !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 6px 8px !important;
|
||||||
|
}
|
||||||
|
.tr .td .input {
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
color: $g19-ghost !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
thead th {
|
thead th {
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
Styles for the Template Variables Manager Panel
|
||||||
|
------------------------------------------------------
|
||||||
|
Accessed via Dashboards
|
||||||
|
*/
|
||||||
|
|
||||||
|
$tvmp-panel-max-width: 1300px;
|
||||||
|
$tvmp-gutter: 30px;
|
||||||
|
$tvmp-min-height: 150px;
|
||||||
|
$tvmp-max-height: calc(100% - 90px);
|
||||||
|
$tvmp-table-gutter: 8px;
|
||||||
|
|
||||||
|
.template-variable-manager {
|
||||||
|
max-width: $tvmp-panel-max-width;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.template-variable-manager--header {
|
||||||
|
height: $chronograf-page-header-height;
|
||||||
|
background: $g0-obsidian;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 $tvmp-gutter;
|
||||||
|
|
||||||
|
.page-header__dismiss {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: -20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.template-variable-manager--body {
|
||||||
|
padding: 18px ($tvmp-gutter - $tvmp-table-gutter) $tvmp-gutter ($tvmp-gutter - $tvmp-table-gutter);
|
||||||
|
border-radius: 0 0 $radius $radius;
|
||||||
|
min-height: $tvmp-min-height;
|
||||||
|
max-height: $tvmp-max-height;
|
||||||
|
@include gradient-v($g2-kevlar,$g0-obsidian);
|
||||||
|
@include custom-scrollbar-round($g0-obsidian,$g3-castle);
|
||||||
|
}
|
||||||
|
.template-variable-manager--table,
|
||||||
|
.template-variable-manager--table-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* Column Widths */
|
||||||
|
.tvm--col-1 {flex: 0 0 140px;}
|
||||||
|
.tvm--col-2 {flex: 0 0 140px;}
|
||||||
|
.tvm--col-3 {flex: 1 0 500px;}
|
||||||
|
.tvm--col-4 {flex: 0 0 160px;}
|
||||||
|
|
||||||
|
/* Table Column Labels */
|
||||||
|
.template-variable-manager--table-heading {
|
||||||
|
padding: 0 $tvmp-table-gutter;
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $g11-sidewalk;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
@include no-user-select();
|
||||||
|
padding-left: 6px;
|
||||||
|
margin-right: $tvmp-table-gutter;
|
||||||
|
&:last-child {margin-right: 0;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Body */
|
||||||
|
.template-variable-manager--table-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.template-variable-manager--table-row {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: $tvmp-table-gutter;
|
||||||
|
transition: background-color 0.25s ease;
|
||||||
|
|
||||||
|
&.editing {
|
||||||
|
background-color: $g3-castle;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: $tvmp-table-gutter;
|
||||||
|
&:last-child {margin-right: 0;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvm-input,
|
||||||
|
.form-control.tvm-input-edit {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 0 6px !important;
|
||||||
|
font-family: $code-font;
|
||||||
|
color: $c-comet !important;
|
||||||
|
height: 30px !important;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus:hover {
|
||||||
|
color: $c-comet !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tvm-input {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 26px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: $radius;
|
||||||
|
border: 2px solid $g5-pepper;
|
||||||
|
background-color: $g2-kevlar;
|
||||||
|
transition:
|
||||||
|
border-color 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: text;
|
||||||
|
border-color: $g6-smoke;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tvm-values,
|
||||||
|
.tvm-values-empty {
|
||||||
|
width: 100%;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: $code-font;
|
||||||
|
color: $c-pool;
|
||||||
|
padding: 0 $tvmp-table-gutter;
|
||||||
|
min-height: 30px;
|
||||||
|
max-height: 90px;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: $radius;
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
margin-top: 2px;
|
||||||
|
@include custom-scrollbar-round($g5-pepper, $g7-graphite);
|
||||||
|
}
|
||||||
|
.tvm-values-empty {
|
||||||
|
color: $g9-mountain;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.tvm-csv-instructions {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: $code-font;
|
||||||
|
padding: 0 $tvmp-table-gutter;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: $radius;
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: $g9-mountain;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvm-query-builder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
> * {margin-right: 2px;}
|
||||||
|
> *:last-child {margin-right: 0;}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
flex: 1 0 0;
|
||||||
|
|
||||||
|
& > .dropdown-toggle {width: 100%;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tvm-query-builder--text {
|
||||||
|
@include no-user-select();
|
||||||
|
background-color: $g5-pepper;
|
||||||
|
border-radius: $radius-small;
|
||||||
|
padding: 0 $tvmp-table-gutter;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: $c-pool;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: $code-font;
|
||||||
|
}
|
||||||
|
.tvm-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
order: 1;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
> span.icon {margin: 0 !important;}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .btn {margin-left: $tvmp-table-gutter;}
|
||||||
|
|
||||||
|
/* Override confirm buttons styles */
|
||||||
|
/* Janky, but doing this quick & dirty for now */
|
||||||
|
.btn-danger {
|
||||||
|
order: 2;
|
||||||
|
height: 30px !important;
|
||||||
|
line-height: 30px !important;
|
||||||
|
padding: 0 9px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.confirm-buttons > .btn {
|
||||||
|
height: 30px !important;
|
||||||
|
width: 30px !important;
|
||||||
|
margin-left: $tvmp-table-gutter !important;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
/* Hide the edit button when confirming a delete */
|
||||||
|
.confirm-buttons + .btn-edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,56 +19,88 @@ $page-header-weight: 400 !important;
|
||||||
background-color: $g0-obsidian;
|
background-color: $g0-obsidian;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
.page-header__container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
max-width: ($page-wrapper-max-width - $page-wrapper-padding - $page-wrapper-padding);
|
||||||
|
}
|
||||||
|
.page-header__left,
|
||||||
|
.page-header__right {
|
||||||
|
flex: 1 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&__container {
|
> *:only-child {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
width: 100%;
|
|
||||||
max-width: ($page-wrapper-max-width - $page-wrapper-padding - $page-wrapper-padding);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
text-transform: none;
|
|
||||||
font-size: $page-header-size;
|
|
||||||
font-weight: $page-header-weight;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
@include no-user-select();
|
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
&__left,
|
}
|
||||||
&__right {
|
.page-header__left {
|
||||||
flex: 1 0 0;
|
justify-content: flex-start;
|
||||||
display: flex;
|
> * {
|
||||||
align-items: center;
|
margin: 0 4px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-header__right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
> * {
|
||||||
|
margin: 0 0 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-header.full-width .page-header__container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.page-header.full-width-no-scrollbar {
|
||||||
|
padding-right: $page-wrapper-padding;
|
||||||
|
|
||||||
> *:only-child {
|
.page-header__container {
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__left {
|
|
||||||
justify-content: flex-start;
|
|
||||||
> * {
|
|
||||||
margin: 0 4px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
> * {
|
|
||||||
margin: 0 0 0 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.full-width .page-header__container {
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
&.full-width-no-scrollbar {
|
}
|
||||||
padding-right: $page-wrapper-padding;
|
.page-header__title {
|
||||||
|
text-transform: none;
|
||||||
|
font-size: $page-header-size;
|
||||||
|
font-weight: $page-header-weight;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
@include no-user-select();
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.page-header__dismiss {
|
||||||
|
width: ($chronograf-page-header-height - 20px);
|
||||||
|
height: ($chronograf-page-header-height - 20px);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.page-header__container {
|
/* Use psuedo elements to render the X */
|
||||||
max-width: 100%;
|
&:before,
|
||||||
}
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 22px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background-color: $g11-sidewalk;
|
||||||
|
transition: background-color 0.25s ease;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
transform: translate(-50%,-50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
transform: translate(-50%,-50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
/* Hover State */
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&:hover:before,
|
||||||
|
&:hover:after {
|
||||||
|
background-color: $g18-cloud;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -33,12 +33,35 @@ $dash-graph-options-arrow: 8px;
|
||||||
Default Dashboard Mode
|
Default Dashboard Mode
|
||||||
------------------------------------------------------
|
------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
.cell-shell {
|
||||||
|
background-color: $g3-castle;
|
||||||
|
border-radius: $radius;
|
||||||
|
border: 2px solid $g3-castle;
|
||||||
|
transition-property: left, top, border-color, background-color;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard {
|
.dashboard {
|
||||||
|
.template-control-bar {
|
||||||
|
height: 50px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $g14-chromium;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
@extend .cell-shell;
|
||||||
|
.dropdown {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
.dropdown-toggle {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.react-grid-item {
|
.react-grid-item {
|
||||||
background-color: $g3-castle;
|
@extend .cell-shell;
|
||||||
border-radius: $radius;
|
|
||||||
border: 2px solid $g3-castle;
|
|
||||||
transition-property: left, top, border-color, background-color;
|
|
||||||
}
|
}
|
||||||
.graph-empty {
|
.graph-empty {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -82,6 +105,7 @@ $dash-graph-options-arrow: 8px;
|
||||||
top: $dash-graph-heading;
|
top: $dash-graph-heading;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
& > div:not(.graph-empty) {
|
& > div:not(.graph-empty) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -353,116 +377,13 @@ $dash-graph-options-arrow: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Cell Edit Mode
|
Overylay Technology (Cell Edit Mode)
|
||||||
------------------------------------------------------
|
------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
$overlay-controls-height: 50px;
|
@import 'overlay-technology';
|
||||||
$overlay-controls-bg: $g2-kevlar;
|
|
||||||
$overlay-bg: rgba($c-pool, 0.7);
|
|
||||||
|
|
||||||
.overlay-technology {
|
/*
|
||||||
position: absolute;
|
Template Variables Manager
|
||||||
top: 0;
|
------------------------------------------------------
|
||||||
bottom: 0;
|
*/
|
||||||
left: 0;
|
@import '../components/template-variables-manager';
|
||||||
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: 16px;
|
|
||||||
border: 0;
|
|
||||||
background-color: $g2-kevlar;
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Graph editing in Dashboards is a little smaller so the dash can be seen in the background */
|
|
||||||
.overlay-technology .resize-container.page-contents {
|
|
||||||
background-image: none !important;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
.overlay-technology .graph {
|
|
||||||
width: 70%;
|
|
||||||
left: 15%;
|
|
||||||
}
|
|
||||||
.overlay-technology .graph-heading,
|
|
||||||
.overlay-technology .graph-container,
|
|
||||||
.overlay-technology .table-container {
|
|
||||||
top: -24px;
|
|
||||||
}
|
|
||||||
.overlay-technology .graph-heading .graph-actions {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
.overlay-technology .graph-container,
|
|
||||||
.overlay-technology .table-container {
|
|
||||||
height: calc(100% - 38px);
|
|
||||||
}
|
|
||||||
.overlay-technology .query-maker {
|
|
||||||
flex: 1 0 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border-radius: 0 0 $radius $radius;
|
|
||||||
background-color: $g2-kevlar;
|
|
||||||
}
|
|
||||||
.overlay-technology .query-maker--tabs {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.overlay-technology .query-maker--tab-contents {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
Styles for Overlay Technology (aka Cell Edit Mode)
|
||||||
|
------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$overlay-controls-height: 50px;
|
||||||
|
$overlay-controls-bg: $g2-kevlar;
|
||||||
|
$overlay-z: 100;
|
||||||
|
|
||||||
|
.overlay-technology {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: $overlay-z;
|
||||||
|
padding: 0 30px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
@include gradient-diag-down($c-pool,$c-comet);
|
||||||
|
opacity: 0.7;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 16px;
|
||||||
|
border: 0;
|
||||||
|
background-color: $g2-kevlar;
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay-controls .confirm-buttons {
|
||||||
|
margin-left: 32px;
|
||||||
|
}
|
||||||
|
.overlay-controls .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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph editing in Dashboards is a little smaller so the dash can be seen in the background */
|
||||||
|
.overlay-technology .resize-container.page-contents {
|
||||||
|
background-image: none !important;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.overlay-technology .graph {
|
||||||
|
width: 70%;
|
||||||
|
left: 15%;
|
||||||
|
}
|
||||||
|
.overlay-technology .graph-heading,
|
||||||
|
.overlay-technology .graph-container,
|
||||||
|
.overlay-technology .table-container {
|
||||||
|
top: -24px;
|
||||||
|
}
|
||||||
|
.overlay-technology .graph-heading .graph-actions {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.overlay-technology .graph-container,
|
||||||
|
.overlay-technology .table-container {
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
}
|
||||||
|
.overlay-technology .query-maker {
|
||||||
|
flex: 1 0 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 0 0 $radius $radius;
|
||||||
|
background-color: $g2-kevlar;
|
||||||
|
}
|
||||||
|
.overlay-technology .query-maker--tabs {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.overlay-technology .query-maker--tab-contents {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import AJAX from 'utils/ajax'
|
import AJAX from 'utils/ajax'
|
||||||
|
|
||||||
export const proxy = async ({source, query, db, rp}) => {
|
export const proxy = async ({source, query, db, rp, tempVars}) => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: source,
|
url: source,
|
||||||
data: {
|
data: {
|
||||||
|
tempVars,
|
||||||
query,
|
query,
|
||||||
db,
|
db,
|
||||||
rp,
|
rp,
|
||||||
|
|
22
ui/yarn.lock
22
ui/yarn.lock
|
@ -4357,6 +4357,10 @@ lodash.merge@^4.4.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
|
||||||
|
|
||||||
|
lodash.mergewith@^4.6.0:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
|
||||||
|
|
||||||
lodash.pick@^4.2.0, lodash.pick@^4.2.1, lodash.pick@^4.4.0:
|
lodash.pick@^4.2.0, lodash.pick@^4.2.1, lodash.pick@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
||||||
|
@ -4784,9 +4788,9 @@ node-pre-gyp@^0.6.29:
|
||||||
tar "~2.2.1"
|
tar "~2.2.1"
|
||||||
tar-pack "~3.3.0"
|
tar-pack "~3.3.0"
|
||||||
|
|
||||||
node-sass@^3.5.3:
|
node-sass@^4.5.2:
|
||||||
version "3.13.1"
|
version "4.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.1.tgz#7240fbbff2396304b4223527ed3020589c004fc2"
|
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64"
|
||||||
dependencies:
|
dependencies:
|
||||||
async-foreach "^0.1.3"
|
async-foreach "^0.1.3"
|
||||||
chalk "^1.1.1"
|
chalk "^1.1.1"
|
||||||
|
@ -4797,13 +4801,15 @@ node-sass@^3.5.3:
|
||||||
in-publish "^2.0.0"
|
in-publish "^2.0.0"
|
||||||
lodash.assign "^4.2.0"
|
lodash.assign "^4.2.0"
|
||||||
lodash.clonedeep "^4.3.2"
|
lodash.clonedeep "^4.3.2"
|
||||||
|
lodash.mergewith "^4.6.0"
|
||||||
meow "^3.7.0"
|
meow "^3.7.0"
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
nan "^2.3.2"
|
nan "^2.3.2"
|
||||||
node-gyp "^3.3.1"
|
node-gyp "^3.3.1"
|
||||||
npmlog "^4.0.0"
|
npmlog "^4.0.0"
|
||||||
request "^2.61.0"
|
request "^2.79.0"
|
||||||
sass-graph "^2.1.1"
|
sass-graph "^2.1.1"
|
||||||
|
stdout-stream "^1.4.0"
|
||||||
|
|
||||||
node-uuid@^1.4.7:
|
node-uuid@^1.4.7:
|
||||||
version "1.4.7"
|
version "1.4.7"
|
||||||
|
@ -6157,7 +6163,7 @@ request-progress@~2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
throttleit "^1.0.0"
|
throttleit "^1.0.0"
|
||||||
|
|
||||||
request@2, request@^2.55.0, request@^2.61.0, request@^2.74.0, request@^2.79.0, request@~2.79.0:
|
request@2, request@^2.55.0, request@^2.74.0, request@^2.79.0, request@~2.79.0:
|
||||||
version "2.79.0"
|
version "2.79.0"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6676,6 +6682,12 @@ sshpk@^1.7.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
||||||
|
|
||||||
|
stdout-stream@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
|
||||||
|
dependencies:
|
||||||
|
readable-stream "^2.0.1"
|
||||||
|
|
||||||
stream-browserify@^2.0.1:
|
stream-browserify@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
|
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
|
||||||
|
|
Loading…
Reference in New Issue