Merge pull request #1304 from influxdata/feature/template-variables

Feature/template variables
pull/10616/head
lukevmorris 2017-04-28 13:24:21 -07:00 committed by GitHub
commit 76195861fc
74 changed files with 4363 additions and 1092 deletions

View File

@ -26,9 +26,13 @@
### 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. [#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. [#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
### UI Improvements

View File

@ -676,7 +676,7 @@
* 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-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)
* 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)

View File

@ -29,7 +29,7 @@ define CHRONOGIRAFFE
,"" _\_
," ## | 0 0.
," ## ,-\__ `.
," / `--._;) - "HAI, I'm Chronogiraffe. Will you be my friend?"
," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!"
," ## /
," ## /
endef

View File

@ -190,10 +190,40 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
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{
ID: int64(d.ID),
Cells: cells,
Templates: templates,
Name: d.Name,
})
}
@ -232,8 +262,44 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
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.Cells = cells
d.Templates = templates
d.Name = pb.Name
return nil
}
@ -300,7 +366,7 @@ func UnmarshalUser(data []byte, u *chronograf.User) error {
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.
func UnmarshalUserPB(data []byte, u *User) error {
if err := proto.Unmarshal(data, u); err != nil {

View File

@ -12,6 +12,9 @@ It has these top-level messages:
Source
Dashboard
DashboardCell
Template
TemplateValue
TemplateQuery
Server
Layout
Cell
@ -59,6 +62,7 @@ type Dashboard struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,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{} }
@ -73,6 +77,13 @@ func (m *Dashboard) GetCells() []*DashboardCell {
return nil
}
func (m *Dashboard) GetTemplates() []*Template {
if m != nil {
return m.Templates
}
return nil
}
type DashboardCell struct {
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
@ -96,6 +107,59 @@ func (m *DashboardCell) GetQueries() []*Query {
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 {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,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) String() string { return proto.CompactTextString(m) }
func (*Server) ProtoMessage() {}
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
type Layout struct {
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) String() string { return proto.CompactTextString(m) }
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 {
if m != nil {
@ -147,7 +211,7 @@ type Cell struct {
func (m *Cell) Reset() { *m = Cell{} }
func (m *Cell) String() string { return proto.CompactTextString(m) }
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 {
if m != nil {
@ -169,7 +233,7 @@ type Query struct {
func (m *Query) Reset() { *m = Query{} }
func (m *Query) String() string { return proto.CompactTextString(m) }
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 {
if m != nil {
@ -186,7 +250,7 @@ type Range struct {
func (m *Range) Reset() { *m = Range{} }
func (m *Range) String() string { return proto.CompactTextString(m) }
func (*Range) ProtoMessage() {}
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
type AlertRule struct {
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) String() string { return proto.CompactTextString(m) }
func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
type User struct {
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) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
func init() {
proto.RegisterType((*Source)(nil), "internal.Source")
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
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((*Layout)(nil), "internal.Layout")
proto.RegisterType((*Cell)(nil), "internal.Cell")
@ -226,47 +293,59 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 670 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xcd, 0x6e, 0xd3, 0x4a,
0x14, 0xd6, 0xc4, 0x76, 0x7e, 0x4e, 0x7b, 0x7b, 0xaf, 0x46, 0x57, 0x30, 0x62, 0x15, 0x59, 0x20,
0x05, 0x24, 0xba, 0xa0, 0x4f, 0x90, 0xd6, 0x12, 0x0a, 0xb4, 0xa5, 0x4c, 0x5a, 0x58, 0x81, 0x34,
0x4d, 0x4f, 0x1a, 0x0b, 0xc7, 0x36, 0x63, 0xbb, 0xa9, 0x5f, 0x81, 0x87, 0x60, 0xc5, 0x8a, 0x25,
0xaf, 0xc2, 0x0b, 0xa1, 0x33, 0x33, 0x76, 0x52, 0x28, 0xa8, 0x2b, 0x76, 0xe7, 0x3b, 0xc7, 0x39,
0x3f, 0xdf, 0xf7, 0x4d, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
0xef, 0x37, 0x38, 0xfc, 0xd4, 0x81, 0xee, 0x34, 0xab, 0xf4, 0x0c, 0xf9, 0x0e, 0x74, 0x26, 0x91,
0x60, 0x43, 0x36, 0xf2, 0x64, 0x67, 0x12, 0x71, 0x0e, 0xfe, 0xb1, 0x5a, 0xa2, 0xe8, 0x0c, 0xd9,
0x68, 0x20, 0x4d, 0x4c, 0xb9, 0xd3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0xf4, 0xcf,
0x0a, 0xea, 0xb6, 0x44, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xa2, 0x8a, 0x62, 0x95, 0xe9, 0x0b,
0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xc9, 0x43, 0xd1, 0x35, 0x69, 0x0a, 0xb9, 0x80,
0x5e, 0x84, 0x73, 0x55, 0x25, 0xa5, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x1b, 0x48, 0x7d, 0x4e, 0x31,
0xc1, 0x4b, 0xad, 0xe6, 0xa2, 0x6f, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa4, 0x05, 0xce, 0x2a,
0x8d, 0xd3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0x3c, 0xaf, 0xc5, 0xc0, 0x34, 0xb8, 0xa5, 0x42, 0x53,
0x8e, 0xb0, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x10, 0xa9, 0x62, 0x71, 0x9e,
0x29, 0x7d, 0x71, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc3, 0x24, 0x29, 0x84, 0x37, 0xf4, 0x46, 0x5b,
0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x30, 0x49, 0xa4, 0xfd, 0x2a, 0xfc, 0xca, 0xe0,
0x9f, 0x1b, 0x05, 0xbe, 0x0d, 0xec, 0xda, 0xcc, 0x08, 0x24, 0xbb, 0x26, 0x54, 0x9b, 0xfe, 0x81,
0x64, 0x35, 0xa1, 0x95, 0x21, 0x3a, 0x90, 0x6c, 0x45, 0x68, 0x61, 0xe8, 0x0d, 0x24, 0x5b, 0xf0,
0xc7, 0xd0, 0xfb, 0x58, 0xa1, 0x8e, 0xb1, 0x10, 0x81, 0x19, 0xfd, 0xef, 0x7a, 0xf4, 0xeb, 0x0a,
0x75, 0x2d, 0x9b, 0x3a, 0xed, 0x6d, 0xa4, 0xb1, 0x3c, 0x9b, 0x98, 0x72, 0x25, 0xc9, 0xd8, 0xb3,
0x39, 0x8a, 0xdd, 0xbd, 0x96, 0xdc, 0xce, 0x24, 0x0a, 0xbf, 0x30, 0xe8, 0x4e, 0x51, 0x5f, 0xa1,
0xbe, 0x13, 0x15, 0x9b, 0x2e, 0xf0, 0xfe, 0xe0, 0x02, 0xff, 0x76, 0x17, 0x04, 0x6b, 0x17, 0xfc,
0x0f, 0xc1, 0x54, 0xcf, 0x26, 0x91, 0xd9, 0xd8, 0x93, 0x16, 0xf0, 0x7b, 0xd0, 0x1d, 0xcf, 0xca,
0xf8, 0x0a, 0x9d, 0x35, 0x1c, 0x0a, 0x3f, 0x33, 0xe8, 0x1e, 0xaa, 0x3a, 0xab, 0xca, 0x8d, 0x35,
0xcd, 0x05, 0x7c, 0x08, 0x5b, 0xe3, 0x3c, 0x4f, 0xe2, 0x99, 0x2a, 0xe3, 0x2c, 0x75, 0xdb, 0x6e,
0xa6, 0xe8, 0x8b, 0x23, 0x54, 0x45, 0xa5, 0x71, 0x89, 0x69, 0xe9, 0xf6, 0xde, 0x4c, 0xf1, 0x87,
0x10, 0x1c, 0x18, 0x85, 0x7d, 0x43, 0xf3, 0xce, 0x9a, 0x66, 0x2b, 0xac, 0x29, 0xd2, 0x81, 0xe3,
0xaa, 0xcc, 0xe6, 0x49, 0xb6, 0x32, 0x97, 0xf4, 0x65, 0x8b, 0xc3, 0xef, 0x0c, 0xfc, 0xbf, 0xa5,
0xf5, 0x36, 0xb0, 0xd8, 0x09, 0xcd, 0xe2, 0x56, 0xf9, 0xde, 0x86, 0xf2, 0x02, 0x7a, 0xb5, 0x56,
0xe9, 0x25, 0x16, 0xa2, 0x3f, 0xf4, 0x46, 0x9e, 0x6c, 0xa0, 0xa9, 0x24, 0xea, 0x1c, 0x93, 0x42,
0x0c, 0x86, 0x1e, 0x3d, 0x0b, 0x07, 0x5b, 0xb7, 0xc0, 0xda, 0x2d, 0xe1, 0x37, 0x06, 0x81, 0x19,
0x4e, 0xbf, 0x3b, 0xc8, 0x96, 0x4b, 0x95, 0x5e, 0x38, 0xea, 0x1b, 0x48, 0x7a, 0x44, 0xfb, 0x8e,
0xf6, 0x4e, 0xb4, 0x4f, 0x58, 0x9e, 0x38, 0x92, 0x3b, 0xf2, 0x84, 0x58, 0x7b, 0xae, 0xb3, 0x2a,
0xdf, 0xaf, 0x2d, 0xbd, 0x03, 0xd9, 0x62, 0x92, 0xfb, 0xed, 0x02, 0xb5, 0xbb, 0x79, 0x20, 0x1d,
0x22, 0x73, 0x1c, 0xd2, 0x56, 0xee, 0x4a, 0x0b, 0xf8, 0x23, 0x08, 0x24, 0x5d, 0x61, 0x4e, 0xbd,
0x41, 0x90, 0x49, 0x4b, 0x5b, 0x0d, 0xf7, 0xdc, 0x67, 0xd4, 0xe5, 0x2c, 0xcf, 0x51, 0x3b, 0x4f,
0x5b, 0x60, 0x7a, 0x67, 0x2b, 0xd4, 0x66, 0x65, 0x4f, 0x5a, 0x10, 0xbe, 0x83, 0xc1, 0x38, 0x41,
0x5d, 0xca, 0x2a, 0xc1, 0x5f, 0x2c, 0xc6, 0xc1, 0x7f, 0x31, 0x7d, 0x75, 0xdc, 0xbc, 0x04, 0x8a,
0xd7, 0xfe, 0xf5, 0x7e, 0xf2, 0xef, 0x4b, 0x95, 0xab, 0x49, 0x64, 0x84, 0xf5, 0xa4, 0x43, 0xe1,
0x13, 0xf0, 0xe9, 0x9d, 0x6c, 0x74, 0xf6, 0x7f, 0xf7, 0xc6, 0xce, 0xbb, 0xe6, 0xdf, 0x7b, 0xef,
0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0xbe, 0xb0, 0xc3, 0xcf, 0x05, 0x00, 0x00,
// 858 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xee, 0x16, 0x34, 0x5a, 0xb1, 0x06, 0x6e, 0x22, 0x0b, 0xa4,
0x82, 0x44, 0x41, 0xec, 0x13, 0xb4, 0xb5, 0x84, 0x42, 0xbb, 0x4b, 0x99, 0xb4, 0xe5, 0x0a, 0xad,
0x26, 0xc9, 0x49, 0x6b, 0xed, 0x24, 0x36, 0x63, 0xbb, 0x59, 0xbf, 0x02, 0x57, 0x3c, 0x01, 0x12,
0x12, 0x57, 0x5c, 0xf2, 0x02, 0x3c, 0x04, 0x2f, 0x84, 0xce, 0xcc, 0xf8, 0x27, 0x6c, 0x41, 0x7b,
0xb5, 0x77, 0xf3, 0x9d, 0x33, 0xf9, 0xe6, 0xfc, 0x7c, 0x9f, 0x03, 0x87, 0xd9, 0xb6, 0x42, 0xbd,
0x95, 0xea, 0xb8, 0xd0, 0x79, 0x95, 0xf3, 0xb0, 0xc5, 0xc9, 0xcf, 0x23, 0x18, 0xcf, 0xf3, 0x5a,
0x2f, 0x91, 0x1f, 0xc2, 0x68, 0x96, 0xc6, 0x6c, 0xca, 0x8e, 0x3c, 0x31, 0x9a, 0xa5, 0x9c, 0x83,
0xff, 0x42, 0x6e, 0x30, 0x1e, 0x4d, 0xd9, 0x51, 0x24, 0xcc, 0x99, 0x62, 0x57, 0x4d, 0x81, 0xb1,
0x67, 0x63, 0x74, 0xe6, 0x1f, 0x41, 0x78, 0x5d, 0x12, 0xdb, 0x06, 0x63, 0xdf, 0xc4, 0x3b, 0x4c,
0xb9, 0x4b, 0x59, 0x96, 0xbb, 0x5c, 0xaf, 0xe2, 0xc0, 0xe6, 0x5a, 0xcc, 0xdf, 0x07, 0xef, 0x5a,
0x5c, 0xc4, 0x63, 0x13, 0xa6, 0x23, 0x8f, 0x61, 0x92, 0xe2, 0x5a, 0xd6, 0xaa, 0x8a, 0x27, 0x53,
0x76, 0x14, 0x8a, 0x16, 0x12, 0xcf, 0x15, 0x2a, 0xbc, 0xd5, 0x72, 0x1d, 0x87, 0x96, 0xa7, 0xc5,
0xfc, 0x18, 0xf8, 0x6c, 0x5b, 0xe2, 0xb2, 0xd6, 0x38, 0x7f, 0x95, 0x15, 0x37, 0xa8, 0xb3, 0x75,
0x13, 0x47, 0x86, 0xe0, 0x81, 0x0c, 0xbd, 0xf2, 0x1c, 0x2b, 0x49, 0x6f, 0x83, 0xa1, 0x6a, 0x61,
0xf2, 0x0b, 0x83, 0x28, 0x95, 0xe5, 0xdd, 0x22, 0x97, 0x7a, 0xf5, 0x56, 0xf3, 0xf8, 0x02, 0x82,
0x25, 0x2a, 0x55, 0xc6, 0xde, 0xd4, 0x3b, 0x3a, 0xf8, 0xfa, 0xe9, 0x71, 0x37, 0xe8, 0x8e, 0xe7,
0x0c, 0x95, 0x12, 0xf6, 0x16, 0xff, 0x0a, 0xa2, 0x0a, 0x37, 0x85, 0x92, 0x15, 0x96, 0xb1, 0x6f,
0x7e, 0xc2, 0xfb, 0x9f, 0x5c, 0xb9, 0x94, 0xe8, 0x2f, 0x25, 0x7f, 0x30, 0x78, 0xbc, 0x47, 0xc5,
0x1f, 0x01, 0x7b, 0x6d, 0xaa, 0x0a, 0x04, 0x7b, 0x4d, 0xa8, 0x31, 0x15, 0x05, 0x82, 0x35, 0x84,
0x76, 0x66, 0x37, 0x81, 0x60, 0x3b, 0x42, 0x77, 0x66, 0x23, 0x81, 0x60, 0x77, 0xfc, 0x33, 0x98,
0xfc, 0x54, 0xa3, 0xce, 0xb0, 0x8c, 0x03, 0xf3, 0xf2, 0x7b, 0xfd, 0xcb, 0xdf, 0xd7, 0xa8, 0x1b,
0xd1, 0xe6, 0xa9, 0x53, 0xb3, 0x4d, 0xbb, 0x1a, 0x73, 0xa6, 0x58, 0x45, 0x9b, 0x9f, 0xd8, 0x18,
0x9d, 0xdd, 0x84, 0xec, 0x3e, 0x46, 0xb3, 0x34, 0xf9, 0x8b, 0xd1, 0x9a, 0x6c, 0xe9, 0x83, 0xf1,
0x99, 0x24, 0xff, 0x10, 0x42, 0x6a, 0xeb, 0xe5, 0xbd, 0xd4, 0x6e, 0x84, 0x13, 0xc2, 0x37, 0x52,
0xf3, 0x2f, 0x61, 0x7c, 0x2f, 0x55, 0x8d, 0x0f, 0x8c, 0xb1, 0xa5, 0xbb, 0xa1, 0xbc, 0x70, 0xd7,
0xba, 0x62, 0xfc, 0x41, 0x31, 0x4f, 0x20, 0x50, 0x72, 0x81, 0xca, 0xe9, 0xcc, 0x02, 0x5a, 0x10,
0x75, 0xd5, 0x98, 0x5e, 0x1e, 0x64, 0xb6, 0xbd, 0xdb, 0x5b, 0xc9, 0x35, 0x3c, 0xde, 0x7b, 0xb1,
0x7b, 0x89, 0xed, 0xbf, 0x64, 0xea, 0x70, 0x6d, 0x58, 0x40, 0x12, 0x2d, 0x51, 0xe1, 0xb2, 0xc2,
0x95, 0x59, 0x41, 0x28, 0x3a, 0x9c, 0xfc, 0xc6, 0x7a, 0x5e, 0xf3, 0x1e, 0x89, 0x70, 0x99, 0x6f,
0x36, 0x72, 0xbb, 0x72, 0xd4, 0x2d, 0xa4, 0xb9, 0xad, 0x16, 0x8e, 0x7a, 0xb4, 0x5a, 0x10, 0xd6,
0x85, 0x33, 0xdc, 0x48, 0x17, 0x7c, 0x0a, 0x07, 0x1b, 0x94, 0x65, 0xad, 0x71, 0x83, 0xdb, 0xca,
0x8d, 0x60, 0x18, 0xe2, 0x4f, 0x61, 0x52, 0xc9, 0xdb, 0x97, 0xaf, 0xb0, 0x71, 0xb3, 0x18, 0x57,
0xf2, 0xf6, 0x1c, 0x1b, 0xfe, 0x31, 0x44, 0xeb, 0x0c, 0xd5, 0xca, 0xa4, 0xec, 0x72, 0x43, 0x13,
0x38, 0xc7, 0x26, 0xf9, 0x9d, 0xc1, 0x78, 0x8e, 0xfa, 0x1e, 0xf5, 0x5b, 0x29, 0x7f, 0xe8, 0x7a,
0xef, 0x7f, 0x5c, 0xef, 0x3f, 0xec, 0xfa, 0xa0, 0x77, 0xfd, 0x13, 0x08, 0xe6, 0x7a, 0x39, 0x4b,
0x4d, 0x45, 0x9e, 0xb0, 0x80, 0x7f, 0x00, 0xe3, 0x93, 0x65, 0x95, 0xdd, 0xa3, 0xfb, 0x14, 0x38,
0x94, 0xfc, 0xca, 0x60, 0x7c, 0x21, 0x9b, 0xbc, 0xae, 0xde, 0x50, 0xd8, 0x14, 0x0e, 0x4e, 0x8a,
0x42, 0x65, 0x4b, 0x59, 0x65, 0xf9, 0xd6, 0x55, 0x3b, 0x0c, 0xd1, 0x8d, 0xe7, 0x83, 0xd9, 0xd9,
0xba, 0x87, 0x21, 0xfe, 0x09, 0x04, 0x67, 0xc6, 0xd0, 0xd6, 0x9d, 0x87, 0xbd, 0x5e, 0xac, 0x8f,
0x4d, 0x92, 0x1a, 0x3c, 0xa9, 0xab, 0x7c, 0xad, 0xf2, 0x9d, 0xe9, 0x24, 0x14, 0x1d, 0x4e, 0xfe,
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,
}

View File

@ -18,6 +18,7 @@ message Dashboard {
int64 ID = 1; // ID is the unique ID 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 Template templates = 4; // Templates replace template variables within InfluxQL
}
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
}
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 {
int64 ID = 1; // ID is the unique ID of the server
string Name = 2; // Name is the user-defined name for the server

View File

@ -123,11 +123,55 @@ type Range struct {
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.
type Query struct {
Command string `json:"query"` // 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.
TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
@ -143,6 +187,16 @@ type DashboardQuery struct {
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
type Response interface {
MarshalJSON() ([]byte, error)
@ -376,6 +430,7 @@ type DashboardID int
type Dashboard struct {
ID DashboardID `json:"id"`
Cells []DashboardCell `json:"cells"`
Templates []Template `json:"templates"`
Name string `json:"name"`
}

View File

@ -68,17 +68,20 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
return nil, err
}
req.Header.Set("Content-Type", "application/json")
c.Logger.
command := q.Command
if len(q.TemplateVars) > 0 {
command = TemplateReplace(q.Command, q.TemplateVars)
}
logs := c.Logger.
WithField("component", "proxy").
WithField("host", req.Host).
WithField("command", q.Command).
WithField("command", command).
WithField("db", q.DB).
WithField("rp", q.RP).
Debug("query")
WithField("rp", q.RP)
logs.Debug("query")
params := req.URL.Query()
params.Set("q", q.Command)
params.Set("q", command)
params.Set("db", q.DB)
params.Set("rp", q.RP)
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 decErr != nil {
c.Logger.
WithField("component", "proxy").
WithField("host", req.Host).
WithField("command", q.Command).
WithField("db", q.DB).
WithField("rp", q.RP).
WithField("influx_status", resp.StatusCode).
logs.WithField("influx_status", resp.StatusCode).
Error("Error parsing results from influxdb: err:", 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
// then send back an error
if resp.StatusCode != http.StatusOK && response.Err != "" {
c.Logger.
WithField("component", "proxy").
WithField("host", req.Host).
WithField("command", q.Command).
WithField("db", q.DB).
WithField("rp", q.RP).
logs.
WithField("influx_status", resp.StatusCode).
Error("Received non-200 response from influxdb")

View File

@ -82,6 +82,7 @@ func Test_Influx_HTTPS_Failure(t *testing.T) {
func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
t.Parallel()
called := false
q := ""
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{}`))
@ -89,6 +90,8 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
values := r.URL.Query()
q = values.Get("q")
}))
defer ts.Close()
@ -118,6 +121,34 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
if called == false {
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) {

22
influx/templates.go Normal file
View File

@ -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
}

133
influx/templates_test.go Normal file
View File

@ -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)
}
})
}
}

261
server/cells.go Normal file
View File

@ -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
}
}
}

View File

@ -5,35 +5,19 @@ import (
"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 dashboardLinks struct {
Self string `json:"self"` // Self link mapping to this resource
Cells string `json:"cells"` // Cells link to the cells endpoint
}
type dashboardCellLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
type dashboardCellResponse struct {
chronograf.DashboardCell
Links dashboardCellLinks `json:"links"`
Templates string `json:"templates"` // Templates link to the templates endpoint
}
type dashboardResponse struct {
ID chronograf.DashboardID `json:"id"`
Cells []dashboardCellResponse `json:"cells"`
Templates []templateResponse `json:"templates"`
Name string `json:"name"`
Links dashboardLinks `json:"links"`
}
@ -46,25 +30,18 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
base := "/chronograf/v1/dashboards"
DashboardDefaults(&d)
AddQueryConfigs(&d)
cells := make([]dashboardCellResponse, len(d.Cells))
for i, cell := range d.Cells {
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),
},
}
}
cells := newCellResponses(d.ID, d.Cells)
templates := newTemplateResponses(d.ID, d.Templates)
return &dashboardResponse{
ID: d.ID,
Name: d.Name,
Cells: cells,
Templates: templates,
Links: dashboardLinks{
Self: fmt.Sprintf("%s/%d", 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 {
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard))
}
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
func ValidDashboardRequest(d *chronograf.Dashboard) error {
for i, c := range d.Cells {
CorrectWidthHeight(&c)
if err := ValidDashboardCellRequest(&c); err != nil {
return err
}
d.Cells[i] = c
}
for _, t := range d.Templates {
if err := ValidTemplateRequest(&t); err != nil {
return err
}
}
DashboardDefaults(d)
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
// if none are specified
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
// objects corresponding to their influxql queries.
func AddQueryConfigs(d *chronograf.Dashboard) {
@ -284,203 +250,3 @@ func AddQueryConfigs(d *chronograf.Dashboard) {
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
}
}
}

View File

@ -233,6 +233,7 @@ func Test_newDashboardResponse(t *testing.T) {
},
},
want: &dashboardResponse{
Templates: []templateResponse{},
Cells: []dashboardCellResponse{
dashboardCellResponse{
Links: dashboardCellLinks{
@ -291,6 +292,7 @@ func Test_newDashboardResponse(t *testing.T) {
Links: dashboardLinks{
Self: "/chronograf/v1/dashboards/0",
Cells: "/chronograf/v1/dashboards/0/cells",
Templates: "/chronograf/v1/dashboards/0/templates",
},
},
},

View File

@ -155,6 +155,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
// 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
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)

View File

@ -8,12 +8,14 @@ import (
"golang.org/x/net/context"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/influx/queries"
)
type QueryRequest struct {
ID string `json:"id"`
Query string `json:"query"`
TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
}
type QueriesRequest struct {
@ -25,6 +27,8 @@ type QueryResponse struct {
Query string `json:"query"`
QueryConfig chronograf.QueryConfig `json:"queryConfig"`
QueryAST *queries.SelectStatement `json:"queryAST,omitempty"`
QueryTemplated *string `json:"queryTemplated,omitempty"`
TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
}
type QueriesResponse struct {
@ -62,17 +66,28 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
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 {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
qr.QueryConfig = qc
if stmt, err := queries.ParseSelect(q.Query); err == nil {
if stmt, err := queries.ParseSelect(query); err == nil {
qr.QueryAST = stmt
}
if len(q.TemplateVars) > 0 {
qr.TemplateVars = q.TemplateVars
qr.QueryConfig.RawText = &qr.Query
}
qr.QueryConfig.ID = q.ID
res.Queries[i] = qr
}

View File

@ -3055,10 +3055,20 @@
"Proxy": {
"type": "object",
"example": {
"query": "select * from cpu where time > now() - 10m",
"query": "select $myfield from cpu where time > now() - 10m",
"db": "telegraf",
"rp": "autogen",
"format": "raw"
"tempVars": [
{
"tempVar": "$myfield",
"values": [
{
"type": "fieldKey",
"value": "usage_user"
}
]
}
]
},
"required": [
"query"
@ -3073,12 +3083,49 @@
"rp": {
"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",
"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."
}
}
},

248
server/templates.go Normal file
View File

@ -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)
}

71
server/templates_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -149,7 +149,7 @@
'eol-last': 0, // TODO: revisit
'id-length': 0,
'id-match': 0,
'indent': [2, 2, {SwitchCase: 1}],
'indent': [0, 2, {SwitchCase: 1}],
'key-spacing': [2, {beforeColon: false, afterColon: true}],
'linebreak-style': [2, 'unix'],
'lines-around-comment': 0,
@ -234,6 +234,6 @@
'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/sort-comp': 0, // TODO: 2
'react/jsx-wrap-multilines': 'error',
'react/jsx-wrap-multilines': ['error', {'declaration': false, 'assignment': false}],
},
}

View File

@ -70,7 +70,7 @@
"mocha": "^2.4.5",
"mocha-loader": "^0.7.1",
"mustache": "^2.2.1",
"node-sass": "^3.5.3",
"node-sass": "^4.5.2",
"postcss-browser-reporter": "^0.4.0",
"postcss-calc": "^5.2.0",
"postcss-loader": "^0.8.0",

View File

@ -10,11 +10,48 @@ import {
editDashboardCell,
renameDashboardCell,
syncDashboardCell,
templateVariableSelected,
} from 'src/dashboards/actions'
let state
const d1 = {id: 1, cells: [], name: "d1"}
const d2 = {id: 2, cells: [], name: "d2"}
const templates = [
{
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 c1 = {
x: 0,
@ -23,9 +60,21 @@ const c1 = {
h: 4,
id: 1,
isEditing: false,
name: "Gigawatts",
name: 'Gigawatts',
}
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', () => {
it('can load the dashboards', () => {
@ -66,6 +115,7 @@ describe('DataExplorer.Reducers.UI', () => {
id: 1,
cells: updatedCells,
name: 'd1',
templates,
}
const actual = reducer(state, updateDashboardCells(d1, updatedCells))
@ -106,7 +156,28 @@ describe('DataExplorer.Reducers.UI', () => {
dashboards: [dash],
}
const actual = reducer(state, renameDashboardCell(dash, 0, 0, "Plutonium Consumption Rate (ug/sec)"))
expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
const actual = reducer(
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)
})
})

View File

@ -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)
})
})
})

View File

@ -12,21 +12,53 @@ import {errorThrown as errorThrownAction} from 'shared/actions/errors'
// 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.
// Routes that do require data nodes can be nested under this component.
const {arrayOf, func, node, shape, string} = PropTypes
const CheckSources = React.createClass({
propTypes: {
children: PropTypes.node,
params: PropTypes.shape({
sourceID: PropTypes.string,
sources: arrayOf(
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,
router: PropTypes.shape({
push: PropTypes.func.isRequired,
})
),
children: node,
params: shape({
sourceID: string,
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
router: shape({
push: func.isRequired,
}).isRequired,
sources: PropTypes.array.isRequired,
errorThrown: PropTypes.func.isRequired,
loadSources: PropTypes.func.isRequired,
location: shape({
pathname: string.isRequired,
}).isRequired,
loadSources: func.isRequired,
errorThrown: 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() {
@ -51,8 +83,8 @@ const CheckSources = React.createClass({
async componentWillUpdate(nextProps, nextState) {
const {router, location, params, errorThrown, sources} = nextProps
const {isFetching} = nextState
const source = sources.find((s) => s.id === params.sourceID)
const defaultSource = sources.find((s) => s.default === true)
const source = sources.find(s => s.id === params.sourceID)
const defaultSource = sources.find(s => s.default === true)
if (!isFetching && !source) {
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
@ -80,15 +112,21 @@ const CheckSources = React.createClass({
render() {
const {params, sources} = this.props
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) {
return <div className="page-spinner" />
}
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, {
return (
this.props.children &&
React.cloneElement(
this.props.children,
Object.assign({}, this.props, {
source,
}))
})
)
)
},
})
@ -96,9 +134,11 @@ const mapStateToProps = ({sources}) => ({
sources,
})
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = dispatch => ({
loadSources: bindActionCreators(loadSourcesAction, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CheckSources))
export default connect(mapStateToProps, mapDispatchToProps)(
withRouter(CheckSources)
)

View File

@ -157,7 +157,7 @@ class AdminPage extends Component {
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>
<h1 className="page-header__title">
Admin
</h1>
</div>

View File

@ -125,7 +125,7 @@ class AlertsApp extends Component {
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>
<h1 className="page-header__title">
Alert History
</h1>
</div>

View File

@ -12,6 +12,8 @@ import {errorThrown} from 'shared/actions/errors'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS',
payload: {
@ -20,28 +22,28 @@ export const loadDashboards = (dashboards, dashboardID) => ({
},
})
export const setTimeRange = (timeRange) => ({
export const setTimeRange = timeRange => ({
type: 'SET_DASHBOARD_TIME_RANGE',
payload: {
timeRange,
},
})
export const updateDashboard = (dashboard) => ({
export const updateDashboard = dashboard => ({
type: 'UPDATE_DASHBOARD',
payload: {
dashboard,
},
})
export const deleteDashboard = (dashboard) => ({
export const deleteDashboard = dashboard => ({
type: 'DELETE_DASHBOARD',
payload: {
dashboard,
},
})
export const deleteDashboardFailed = (dashboard) => ({
export const deleteDashboardFailed = dashboard => ({
type: 'DELETE_DASHBOARD_FAILED',
payload: {
dashboard,
@ -96,9 +98,10 @@ export const renameDashboardCell = (dashboard, x, y, name) => ({
},
})
export const deleteDashboardCell = (cell) => ({
export const deleteDashboardCell = (dashboard, cell) => ({
type: 'DELETE_DASHBOARD_CELL',
payload: {
dashboard,
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
export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
export const getDashboardsAsync = () => async dispatch => {
try {
const {data: {dashboards}} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards, dashboardID))
dispatch(loadDashboards(dashboards))
} catch (error) {
dispatch(errorThrown(error))
console.error(error)
throw error
dispatch(errorThrown(error))
}
}
export const putDashboard = (dashboard) => async (dispatch) => {
export const putDashboard = dashboard => async dispatch => {
try {
const {data} = await updateDashboardAJAX(dashboard)
dispatch(updateDashboard(data))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateDashboardCell = (dashboard, cell) => async (dispatch) => {
export const updateDashboardCell = (dashboard, cell) => async dispatch => {
try {
const {data} = await updateDashboardCellAJAX(cell)
dispatch(syncDashboardCell(dashboard, data))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardAsync = (dashboard) => async (dispatch) => {
export const deleteDashboardAsync = dashboard => async dispatch => {
dispatch(deleteDashboard(dashboard))
try {
await deleteDashboardAJAX(dashboard)
dispatch(publishAutoDismissingNotification('success', 'Dashboard deleted successfully.'))
dispatch(
publishAutoDismissingNotification(
'success',
'Dashboard deleted successfully.'
)
)
} 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))
}
}
export const addDashboardCellAsync = (dashboard) => async (dispatch) => {
export const addDashboardCellAsync = dashboard => async dispatch => {
try {
const {data} = await addDashboardCellAJAX(dashboard, NEW_DEFAULT_DASHBOARD_CELL)
const {data} = await addDashboardCellAJAX(
dashboard,
NEW_DEFAULT_DASHBOARD_CELL
)
dispatch(addDashboardCell(dashboard, data))
} catch (error) {
dispatch(errorThrown(error))
console.error(error)
throw error
dispatch(errorThrown(error))
}
}
export const deleteDashboardCellAsync = (cell) => async (dispatch) => {
export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
try {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(cell))
dispatch(deleteDashboardCell(dashboard, cell))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
throw error
}
}

View File

@ -1,4 +1,5 @@
import AJAX from 'utils/ajax'
import {proxy} from 'utils/queryUrlGenerator'
export function getDashboards() {
return AJAX({
@ -23,7 +24,7 @@ export function updateDashboardCell(cell) {
})
}
export const createDashboard = async (dashboard) => {
export const createDashboard = async dashboard => {
try {
return await AJAX({
method: 'POST',
@ -36,7 +37,7 @@ export const createDashboard = async (dashboard) => {
}
}
export const deleteDashboard = async (dashboard) => {
export const deleteDashboard = async dashboard => {
try {
return await AJAX({
method: 'DELETE',
@ -61,7 +62,7 @@ export const addDashboardCell = async (dashboard, cell) => {
}
}
export const deleteDashboardCell = async (cell) => {
export const deleteDashboardCell = async cell => {
try {
return await AJAX({
method: 'DELETE',
@ -72,3 +73,34 @@ export const deleteDashboardCell = async (cell) => {
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
}
}

View File

@ -131,6 +131,7 @@ class CellEditorOverlay extends Component {
const {
source,
onCancel,
templates,
timeRange,
autoRefresh,
editQueryStatus,
@ -174,6 +175,7 @@ class CellEditorOverlay extends Component {
/>
<QueryMaker
source={source}
templates={templates}
queries={queriesWorkingDraft}
actions={queryActions}
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 = {
onCancel: func.isRequired,
onSave: func.isRequired,
cell: shape({}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
).isRequired,
timeRange: shape({
upper: string,
lower: string,
@ -203,9 +210,10 @@ CellEditorOverlay.propTypes = {
autoRefresh: number.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
queries: string.isRequired,
}),
}),
}).isRequired,
}).isRequired,
editQueryStatus: func.isRequired,
queryStatus: shape({
queryID: string,

View File

@ -1,31 +1,37 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import omit from 'lodash/omit'
import LayoutRenderer from 'shared/components/LayoutRenderer'
import Dropdown from 'shared/components/Dropdown'
const Dashboard = ({
source,
timeRange,
dashboard,
isEditMode,
inPresentationMode,
onAddCell,
onPositionChange,
onEditCell,
autoRefresh,
onRenameCell,
onUpdateCell,
onDeleteCell,
onPositionChange,
inPresentationMode,
onOpenTemplateManager,
onSummonOverlayTechnologies,
source,
autoRefresh,
timeRange,
onSelectTemplate,
}) => {
if (dashboard.id === 0) {
return null
}
const cells = dashboard.cells.map((cell) => {
const {templates} = dashboard
const cells = dashboard.cells.map(cell => {
const dashboardCell = {...cell}
dashboardCell.queries = dashboardCell.queries.map(({label, query, queryConfig, db}) =>
({
dashboardCell.queries = dashboardCell.queries.map(
({label, query, queryConfig, db}) => ({
label,
query,
queryConfig,
@ -38,11 +44,47 @@ const Dashboard = ({
})
return (
<div className={classnames({'page-contents': true, 'presentation-mode': inPresentationMode})}>
{cells.length ?
<div className={classnames('container-fluid full-width dashboard', {'dashboard-edit': isEditMode})}>
<LayoutRenderer
<div
className={classnames(
'dashboard container-fluid full-width page-contents',
{'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}
templates={templates}
cells={cells}
autoRefresh={autoRefresh}
source={source.links.proxy}
@ -53,32 +95,20 @@ const Dashboard = ({
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
</div> :
<div className="dashboard__empty">
: <div className="dashboard__empty">
<p>This Dashboard has no Graphs</p>
<button
className="btn btn-primary btn-m"
onClick={onAddCell}
>
<button className="btn btn-primary btn-m" onClick={onAddCell}>
Add Graph
</button>
</div>
}
</div>}
</div>
)
}
const {
bool,
func,
shape,
string,
number,
} = PropTypes
const {arrayOf, bool, func, shape, string, number} = PropTypes
Dashboard.propTypes = {
dashboard: shape({}).isRequired,
isEditMode: bool,
inPresentationMode: bool,
onAddCell: func,
onPositionChange: func,
@ -94,6 +124,26 @@ Dashboard.propTypes = {
}).isRequired,
autoRefresh: number.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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -26,3 +26,49 @@ export const NEW_DASHBOARD = {
name: 'Name This Dashboard',
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

View File

@ -1,85 +1,54 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import OverlayTechnologies from 'src/shared/components/OverlayTechnologies'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
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 {setAutoRefresh} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
const {
arrayOf,
bool,
func,
number,
shape,
string,
} = PropTypes
class DashboardPage extends Component {
constructor(props) {
super(props)
const DashboardPage = React.createClass({
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 {
this.state = {
selectedCell: null,
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() {
const {
@ -88,78 +57,159 @@ const DashboardPage = React.createClass({
} = this.props
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() {
this.setState({selectedCell: null})
},
}
handleSaveEditedCell(newCell) {
this.props.dashboardActions.updateDashboardCell(this.getActiveDashboard(), newCell)
this.props.dashboardActions
.updateDashboardCell(this.getActiveDashboard(), newCell)
.then(this.handleDismissOverlay)
},
}
handleSummonOverlayTechnologies(cell) {
this.setState({selectedCell: cell})
},
}
handleChooseTimeRange({lower}) {
this.props.dashboardActions.setTimeRange({lower, upper: null})
},
}
handleUpdatePosition(cells) {
const newDashboard = {...this.getActiveDashboard(), cells}
this.props.dashboardActions.updateDashboard(newDashboard)
this.props.dashboardActions.putDashboard(newDashboard)
},
}
handleAddCell() {
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
},
}
handleEditDashboard() {
this.setState({isEditMode: true})
},
}
handleCancelEditDashboard() {
this.setState({isEditMode: false})
},
}
handleRenameDashboard(name) {
this.setState({isEditMode: false})
const newDashboard = {...this.getActiveDashboard(), name}
this.props.dashboardActions.updateDashboard(newDashboard)
this.props.dashboardActions.putDashboard(newDashboard)
},
}
// Places cell into editing mode.
handleEditDashboardCell(x, y, isEditing) {
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) {
return (evt) => {
this.props.dashboardActions.renameDashboardCell(this.getActiveDashboard(), x, y, evt.target.value)
return evt => {
this.props.dashboardActions.renameDashboardCell(
this.getActiveDashboard(),
x,
y,
evt.target.value
)
}
}
},
handleUpdateDashboardCell(newCell) {
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())
}
},
}
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() {
const {params: {dashboardID}, dashboards} = this.props
return dashboards.find(d => d.id === +dashboardID)
},
}
render() {
const {
@ -177,35 +227,41 @@ const DashboardPage = React.createClass({
const dashboard = dashboards.find(d => d.id === +dashboardID)
const {
selectedCell,
isEditMode,
} = this.state
const {selectedCell, isEditMode, isTemplating} = this.state
return (
<div className="page">
{
selectedCell ?
<CellEditorOverlay
{isTemplating
? <OverlayTechnologies>
<TemplateVariableManager
onClose={this.handleCloseTemplateManager}
onEditTemplateVariables={this.handleEditTemplateVariables}
source={source}
templates={dashboard.templates}
onRunQueryFailure={this.handleRunQueryFailure}
/>
</OverlayTechnologies>
: null}
{selectedCell
? <CellEditorOverlay
source={source}
templates={dashboard.templates}
cell={selectedCell}
autoRefresh={autoRefresh}
timeRange={timeRange}
onCancel={this.handleDismissOverlay}
onSave={this.handleSaveEditedCell}
editQueryStatus={dashboardActions.editCellQueryStatus}
autoRefresh={autoRefresh}
queryStatus={cellQueryStatus}
/> :
null
}
{
isEditMode ?
<DashboardHeaderEdit
onSave={this.handleSaveEditedCell}
onCancel={this.handleDismissOverlay}
editQueryStatus={dashboardActions.editCellQueryStatus}
/>
: null}
{isEditMode
? <DashboardHeaderEdit
dashboard={dashboard}
onCancel={this.handleCancelEditDashboard}
onSave={this.handleRenameDashboard}
/> :
<DashboardHeader
/>
: <DashboardHeader
buttonText={dashboard ? dashboard.name : ''}
handleChooseAutoRefresh={handleChooseAutoRefresh}
autoRefresh={autoRefresh}
@ -219,55 +275,106 @@ const DashboardPage = React.createClass({
onAddCell={this.handleAddCell}
onEditDashboard={this.handleEditDashboard}
>
{
dashboards ?
dashboards.map((d, i) => {
return (
{dashboards
? dashboards.map((d, 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}
</Link>
</li>
)
}) :
null
}
</DashboardHeader>
}
{
dashboard ?
<Dashboard
dashboard={dashboard}
inPresentationMode={inPresentationMode}
))
: null}
</DashboardHeader>}
{dashboard
? <Dashboard
source={source}
autoRefresh={autoRefresh}
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
onAddCell={this.handleAddCell}
onPositionChange={this.handleUpdatePosition}
inPresentationMode={inPresentationMode}
onEditCell={this.handleEditDashboardCell}
onPositionChange={this.handleUpdatePosition}
onDeleteCell={this.handleDeleteDashboardCell}
onRenameCell={this.handleRenameDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
onDeleteCell={this.handleDeleteDashboardCell}
onOpenTemplateManager={this.handleOpenTemplateManager}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/> :
null
}
onSelectTemplate={this.handleSelectTemplate}
/>
: null}
</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 {
app: {
ephemeral: {inPresentationMode},
persisted: {autoRefresh},
},
dashboardUI: {
dashboards,
timeRange,
cellQueryStatus,
},
app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}},
dashboardUI: {dashboards, timeRange, cellQueryStatus},
} = state
return {
@ -279,10 +386,11 @@ const mapStateToProps = (state) => {
}
}
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = dispatch => ({
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage)

View File

@ -10,6 +10,8 @@ const initialState = {
cellQueryStatus: {queryID: null, status: null},
}
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
export default function ui(state = initialState, action) {
switch (action.type) {
case 'LOAD_DASHBOARDS': {
@ -30,8 +32,9 @@ export default function ui(state = initialState, action) {
case 'UPDATE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {
dashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? dashboard : d),
dashboards: state.dashboards.map(
d => (d.id === dashboard.id ? dashboard : d)
),
}
return {...state, ...newState}
@ -40,7 +43,7 @@ export default function ui(state = initialState, action) {
case 'DELETE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {
dashboards: state.dashboards.filter((d) => d.id !== dashboard.id),
dashboards: state.dashboards.filter(d => d.id !== dashboard.id),
}
return {...state, ...newState}
@ -49,10 +52,7 @@ export default function ui(state = initialState, action) {
case 'DELETE_DASHBOARD_FAILED': {
const {dashboard} = action.payload
const newState = {
dashboards: [
_.cloneDeep(dashboard),
...state.dashboards,
],
dashboards: [_.cloneDeep(dashboard), ...state.dashboards],
}
return {...state, ...newState}
}
@ -66,7 +66,9 @@ export default function ui(state = initialState, action) {
}
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}
@ -78,7 +80,9 @@ export default function ui(state = initialState, action) {
const newCells = [cell, ...dashboard.cells]
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}
return {...state, ...newState}
@ -87,7 +91,7 @@ export default function ui(state = initialState, action) {
case 'EDIT_DASHBOARD_CELL': {
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 = {
...cell,
@ -96,27 +100,32 @@ export default function ui(state = initialState, action) {
const newDashboard = {
...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 = {
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}
}
case 'DELETE_DASHBOARD_CELL': {
const {cell} = action.payload
const {dashboard} = state
const {dashboard, cell} = action.payload
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 = {
...dashboard,
cells: newCells,
}
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}
@ -127,11 +136,15 @@ export default function ui(state = initialState, action) {
const newDashboard = {
...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 = {
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}
@ -140,7 +153,7 @@ export default function ui(state = initialState, action) {
case 'RENAME_DASHBOARD_CELL': {
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 = {
...cell,
@ -149,11 +162,13 @@ export default function ui(state = initialState, action) {
const newDashboard = {
...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 = {
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}
@ -164,6 +179,37 @@ export default function ui(state = initialState, action) {
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

View File

@ -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

View File

@ -7,11 +7,7 @@ import TagList from './TagList'
import QueryEditor from './QueryEditor'
import buildInfluxQLQuery from 'utils/influxql'
const {
string,
shape,
func,
} = PropTypes
const {arrayOf, func, shape, string} = PropTypes
const QueryBuilder = React.createClass({
propTypes: {
@ -27,6 +23,11 @@ const QueryBuilder = React.createClass({
upper: string,
lower: string,
}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
@ -78,12 +79,12 @@ const QueryBuilder = React.createClass({
},
render() {
const {query, timeRange} = this.props
const {query, timeRange, templates} = this.props
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
return (
<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()}
</div>
)

View File

@ -1,66 +1,195 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import classNames from 'classnames'
import Dropdown from 'src/shared/components/Dropdown'
import LoadingDots from 'src/shared/components/LoadingDots'
import TemplateDrawer from 'src/shared/components/TemplateDrawer'
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
import {TEMPLATE_MATCHER} from 'src/dashboards/constants'
const ENTER = 13
const ESCAPE = 27
const {bool, func, shape, string} = PropTypes
const QueryEditor = React.createClass({
propTypes: {
query: string.isRequired,
onUpdate: func.isRequired,
config: shape({
status: shape({
error: string,
loading: bool,
success: string,
warn: string,
}),
}).isRequired,
},
getInitialState() {
return {
class QueryEditor extends Component {
constructor(props) {
super(props)
this.state = {
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) {
if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
},
}
handleKeyDown(e) {
if (e.keyCode === ENTER) {
e.preventDefault()
this.handleUpdate()
} else if (e.keyCode === ESCAPE) {
this.setState({value: this.state.value}, () => {
this.editor.blur()
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) {
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()
this.handleUpdate()
}
}
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() {
const {templates} = this.props
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({
value: this.editor.value,
isTemplating: true,
selectedTemplate: newTemplate,
filteredTemplates,
value,
})
},
this.editor.setSelectionRange(start, end)
} else {
this.setState({isTemplating: false, value})
}
}
handleUpdate() {
this.props.onUpdate(this.state.value)
},
}
handleChooseTemplate(template) {
this.setState({value: template.query})
},
}
handleSelectTempVar(tempVar) {
this.setState({selectedTemplate: tempVar})
}
render() {
const {config: {status}} = this.props
const {value} = this.state
const {
value,
isTemplating,
selectedTemplate,
filteredTemplates,
} = this.state
return (
<div className="query-editor">
@ -69,13 +198,38 @@ const QueryEditor = React.createClass({
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleUpdate}
ref={editor => (this.editor = editor)}
ref={editor => this.editor = editor}
value={value}
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
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'}
@ -84,27 +238,29 @@ const QueryEditor = React.createClass({
/>
</div>
)
},
renderStatus(status) {
if (!status) {
return <div className="query-editor--status" />
}
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={classNames('query-editor--status', {
'query-editor--error': status.error,
'query-editor--success': status.success,
'query-editor--warning': status.warn,
<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
@ -115,9 +271,29 @@ const QueryEditor = React.createClass({
})}
/>
{status.error || status.warn || status.success}
</span>
<Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseTemplate}
className="query-editor--templates"
/>
</div>
)
},
})
}
}
const {arrayOf, func, shape, string} = PropTypes
QueryEditor.propTypes = {
query: string.isRequired,
onUpdate: func.isRequired,
config: shape().isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
}
export default QueryEditor

View File

@ -4,14 +4,7 @@ import QueryBuilder from './QueryBuilder'
import QueryMakerTab from './QueryMakerTab'
import buildInfluxQLQuery from 'utils/influxql'
const {
arrayOf,
func,
node,
number,
shape,
string,
} = PropTypes
const {arrayOf, func, node, number, shape, string} = PropTypes
const QueryMaker = React.createClass({
propTypes: {
@ -25,6 +18,11 @@ const QueryMaker = React.createClass({
upper: string,
lower: string,
}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
@ -76,7 +74,7 @@ const QueryMaker = React.createClass({
},
renderQueryBuilder() {
const {timeRange, actions, source} = this.props
const {timeRange, actions, source, templates} = this.props
const query = this.getActiveQuery()
if (!query) {
@ -93,6 +91,7 @@ const QueryMaker = React.createClass({
<QueryBuilder
source={source}
timeRange={timeRange}
templates={templates}
query={query}
actions={actions}
onAddQuery={this.handleAddQuery}

View File

@ -85,8 +85,7 @@ const Visualization = React.createClass({
editQueryStatus,
activeQueryIndex,
} = this.props
const {source} = this.context
const proxyLink = source.links.proxy
const {source: {links: {proxy}}} = this.context
const {view} = this.state
const statements = queryConfigs.map(query => {
@ -94,7 +93,7 @@ const Visualization = React.createClass({
return {text, id: query.id}
})
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 (

View File

@ -43,7 +43,9 @@ const Header = React.createClass({
<div className="page-header full-width-no-scrollbar">
<div className="page-header__container">
<div className="page-header__left">
<h1>Data Explorer</h1>
<h1 className="page-header__title">
Data Explorer
</h1>
</div>
<div className="page-header__right">
<GraphTips />

View File

@ -78,7 +78,7 @@ export const HostsPage = React.createClass({
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>
<h1 className="page-header__title">
Host List
</h1>
</div>

View File

@ -11,7 +11,7 @@ class KapacitorForm extends Component {
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>
<h1 className="page-header__title">
Configure Kapacitor
</h1>
</div>

View File

@ -50,7 +50,7 @@ const PageContents = ({children, source}) => (
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>Kapacitor Rules</h1>
<h1 className="page-header__title">Kapacitor Rules</h1>
</div>
<div className="page-header__right">
<SourceIndicator sourceName={source && source.name} />

View File

@ -14,7 +14,9 @@ export const handleSuccess = (data, query, editQueryStatus) => {
const series = _.get(results, ['0', 'series'], false)
// 200 from server and no results = warn
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
}
@ -37,10 +39,19 @@ export const handleError = (error, query, editQueryStatus) => {
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)
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)
} catch (error) {
errorThrown(error)

View File

@ -2,7 +2,7 @@ import AJAX from 'utils/ajax'
import _ from 'lodash'
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
export const showDatabases = async (source) => {
export const showDatabases = async source => {
const query = 'SHOW DATABASES'
return await proxy({source, query})
}
@ -10,7 +10,7 @@ export const showDatabases = async (source) => {
export const showRetentionPolicies = async (source, databases) => {
let query
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 {
query = `SHOW RETENTION POLICIES ON "${databases}"`
}
@ -30,24 +30,35 @@ export function killQuery(source, queryId) {
return proxy({source, query})
}
export function showMeasurements(source, db) {
export const showMeasurements = async (source, db) => {
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 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}) {
const keys = tagKeys.sort().map((k) => `"${k}"`).join(', ')
export const showTagValues = async ({
source,
database,
retentionPolicy,
measurement,
tagKeys,
}) => {
const keys = tagKeys.sort().map(k => `"${k}"`).join(', ')
const rp = _.toString(retentionPolicy)
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() {
@ -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 url = buildInfluxUrl({host, statement})
@ -70,8 +88,8 @@ export function dropShard(host, shard, 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}"`
return proxy({source, query, db})
return await proxy({source, query, db})
}

View File

@ -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

View File

@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
const {
arrayOf,
bool,
element,
func,
number,
@ -12,82 +13,141 @@ const {
string,
} = PropTypes
const AutoRefresh = (ComposedComponent) => {
const AutoRefresh = ComposedComponent => {
const wrapper = React.createClass({
propTypes: {
children: element,
autoRefresh: number.isRequired,
queries: arrayOf(shape({
templates: arrayOf(
shape({
type: string.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,
}).isRequired
).isRequired,
editQueryStatus: func,
},
getInitialState() {
return {
lastQuerySuccessful: false,
timeSeries: [],
}
},
componentDidMount() {
const {queries, autoRefresh} = this.props
this.executeQueries(queries)
if (autoRefresh) {
this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh)
this.intervalID = setInterval(
() => this.executeQueries(queries),
autoRefresh
)
}
},
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) {
this.executeQueries(nextProps.queries)
}
if ((this.props.autoRefresh !== nextProps.autoRefresh) || shouldRefetch) {
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
clearInterval(this.intervalID)
if (nextProps.autoRefresh) {
this.intervalID = setInterval(() => this.executeQueries(nextProps.queries), nextProps.autoRefresh)
this.intervalID = setInterval(
() => this.executeQueries(nextProps.queries),
nextProps.autoRefresh
)
}
}
},
queryDifference(left, right) {
const leftStrs = left.map((q) => `${q.host}${q.text}`)
const rightStrs = right.map((q) => `${q.host}${q.text}`)
return _.difference(_.union(leftStrs, rightStrs), _.intersection(leftStrs, rightStrs))
const leftStrs = left.map(q => `${q.host}${q.text}`)
const rightStrs = right.map(q => `${q.host}${q.text}`)
return _.difference(
_.union(leftStrs, rightStrs),
_.intersection(leftStrs, rightStrs)
)
},
async executeQueries(queries) {
executeQueries(queries) {
const {templates = [], editQueryStatus} = this.props
if (!queries.length) {
this.setState({
timeSeries: [],
})
this.setState({timeSeries: []})
return
}
this.setState({isFetching: true})
let count = 0
const newSeries = []
for (const query of queries) {
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
// TODO: may need to make this a try/catch
const response = await fetchTimeSeriesAsync({source: host, db: database, rp, query}, this.props.editQueryStatus)
newSeries.push({response})
count += 1
if (count === queries.length) {
const querySuccessful = !this._noResultsForQuery(newSeries)
this.setState({
lastQuerySuccessful: querySuccessful,
isFetching: false,
timeSeries: newSeries,
const selectedTempVarTemplates = templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
}
const timeSeriesPromises = queries.map(query => {
const {host, database, rp} = query
return fetchTimeSeriesAsync(
{
source: host,
db: database,
rp,
query,
tempVars: selectedTempVarTemplates,
},
editQueryStatus
)
})
Promise.all(timeSeriesPromises).then(timeSeries => {
const newSeries = timeSeries.map(response => ({response}))
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
this.setState({
timeSeries: newSeries,
lastQuerySuccessful,
isFetching: false,
})
})
},
componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = false
},
render() {
const {timeSeries} = this.state
@ -95,16 +155,14 @@ const AutoRefresh = (ComposedComponent) => {
return this.renderFetching(timeSeries)
}
if (this._noResultsForQuery(timeSeries) || !this.state.lastQuerySuccessful) {
if (
this._noResultsForQuery(timeSeries) ||
!this.state.lastQuerySuccessful
) {
return this.renderNoResults()
}
return (
<ComposedComponent
{...this.props}
data={timeSeries}
/>
)
return <ComposedComponent {...this.props} data={timeSeries} />
},
/**
@ -140,8 +198,8 @@ const AutoRefresh = (ComposedComponent) => {
return true
}
return data.every((datum) => {
return datum.response.results.every((result) => {
return data.every(datum => {
return datum.response.results.every(result => {
return Object.keys(result).length === 0
})
})

View File

@ -4,7 +4,10 @@ import OnClickOutside from 'shared/components/OnClickOutside'
import ConfirmButtons from 'shared/components/ConfirmButtons'
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
</button>
)
@ -35,23 +38,24 @@ class DeleteConfirmButtons extends Component {
const {onDelete, item} = this.props
const {isConfirming} = this.state
return isConfirming ?
<ConfirmButtons onConfirm={onDelete} item={item} onCancel={this.handleCancel} /> :
<DeleteButton onClickDelete={this.handleClickDelete} />
return isConfirming
? <ConfirmButtons
onConfirm={onDelete}
item={item}
onCancel={this.handleCancel}
/>
: <DeleteButton onClickDelete={this.handleClickDelete} />
}
}
const {
func,
shape,
} = PropTypes
const {func, oneOfType, shape, string} = PropTypes
DeleteButton.propTypes = {
onClickDelete: func.isRequired,
}
DeleteConfirmButtons.propTypes = {
item: shape({}),
item: oneOfType([(string, shape())]),
onDelete: func.isRequired,
}

View File

@ -1,7 +1,6 @@
import React, {Component, PropTypes} from 'react'
import {Link} from 'react-router'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
class Dropdown extends Component {
@ -134,6 +133,7 @@ Dropdown.propTypes = {
})
).isRequired,
onChoose: func.isRequired,
onClick: func,
addNew: shape({
url: string.isRequired,
text: string.isRequired,

View File

@ -46,6 +46,7 @@ export const LayoutRenderer = React.createClass({
type: string.isRequired,
}).isRequired
),
templates: arrayOf(shape()).isRequired,
host: string,
source: string,
onPositionChange: func,
@ -89,12 +90,41 @@ export const LayoutRenderer = React.createClass({
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() {
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) => {
const qs = cell.queries.map((query) => {
// TODO: Canned dashboards use an old query schema,
const queries = cell.queries.map((query) => {
// TODO: Canned dashboards (and possibly Kubernetes dashboard) use an old query schema,
// which does not have enough information for the new `buildInfluxQLQuery` function
// to operate on. We will use `buildQueryForOldQuerySchema` until we conform
// on a stable query representation.
@ -112,7 +142,6 @@ export const LayoutRenderer = React.createClass({
})
})
if (cell.type === 'single-stat') {
return (
<div key={cell.i}>
<NameableGraph
@ -124,34 +153,7 @@ export const LayoutRenderer = React.createClass({
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 (
<div key={cell.i}>
<NameableGraph
onEditCell={onEditCell}
onRenameCell={onRenameCell}
onUpdateCell={onUpdateCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
shouldNotBeEditable={shouldNotBeEditable}
cell={cell}
>
<RefreshingLineGraph
queries={qs}
autoRefresh={autoRefresh}
showSingleStat={cell.type === 'line-plus-single-stat'}
displayOptions={displayOptions}
/>
{this.renderRefreshingGraph(cell.type, queries)}
</NameableGraph>
</div>
)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1 @@
export const TEMPLATE_VARIABLE_SELECTED = 'TEMPLATE_VARIABLE_SELECTED'

View File

@ -384,7 +384,7 @@ export const HEARTBEAT_INTERVAL = 10000 // ms
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // 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

View File

@ -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

View File

@ -2,7 +2,7 @@ export default function parseShowFieldKeys(response) {
const errors = []
const fieldSets = {}
response.results.forEach((result) => {
response.results.forEach(result => {
if (result.error) {
errors.push(result.error)
return
@ -14,7 +14,7 @@ export default function parseShowFieldKeys(response) {
const series = result.series[0]
const fieldKeyIndex = series.columns.indexOf('fieldKey')
const fields = series.values.map((value) => {
const fields = series.values.map(value => {
return value[fieldKeyIndex]
})
const measurement = series.name

View File

@ -21,7 +21,7 @@ export default function parseShowMeasurements(response) {
const series = result.series[0]
const measurementNameIndex = series.columns.indexOf('name')
const measurements = series.values.map((value) => value[measurementNameIndex])
const measurements = series.values.map(value => value[measurementNameIndex])
measurementSets.push({
index,
@ -34,4 +34,3 @@ export default function parseShowMeasurements(response) {
measurementSets,
}
}

View File

@ -49,7 +49,7 @@ class ManageSources extends Component {
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>Configuration</h1>
<h1 className="page-header__title">Configuration</h1>
</div>
</div>
</div>

View File

@ -115,7 +115,7 @@ export const SourcePage = React.createClass({
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1>
<h1 className="page-header__title">
{editMode ? 'Edit Source' : 'Add a New Source'}
</h1>
</div>

View File

@ -11,19 +11,10 @@
border-radius: 0 $radius 0 0;
background-color: $query-editor--bg;
position: relative;
}
.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;
z-index: 2; /* Minimum amount to obcure the toggle flip within Query Builder. Will fix later */
}
.query-editor--field {
font-family: $code-font;
font-size: 12px;
line-height: 14px;
font-weight: 600;
@ -34,7 +25,13 @@
resize: none;
width: 100%;
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;
background-color: $query-editor--field-bg;
color: $query-editor--field-text;
padding: 12px 10px 0 10px;
border-radius: $radius $radius 0 0;
@ -49,33 +46,16 @@
color: $query-editor--field-text !important;
border-color: $c-pool;
}
&:focus + .query-editor--status {
&:focus + .varmoji {
border-color: $c-pool;
}
}
.query-editor--status {
display: flex;
align-items: center;
justify-content: flex-end;
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-dots {
bottom: $query-editor--templates-offset;
@ -83,10 +63,30 @@
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 {
position: absolute;
bottom: ($query-editor--templates-offset - 8px);
right: $query-editor--templates-offset;
margin: 0 4px 0 0 ;
div.dropdown-toggle.btn.btn-sm {
width: $query-editor--templates-width;
@ -103,6 +103,88 @@
min-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 {
background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%);
}

View File

@ -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 {
thead th {

View File

@ -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;
}
}

View File

@ -19,8 +19,8 @@ $page-header-weight: 400 !important;
background-color: $g0-obsidian;
border: none;
margin: 0;
&__container {
}
.page-header__container {
width: 100%;
display: flex;
align-items: center;
@ -28,8 +28,40 @@ $page-header-weight: 400 !important;
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;
> *:only-child {
margin: 0;
}
h1 {
}
.page-header__left {
justify-content: flex-start;
> * {
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;
.page-header__container {
max-width: 100%;
}
}
.page-header__title {
text-transform: none;
font-size: $page-header-size;
font-weight: $page-header-weight;
@ -38,37 +70,37 @@ $page-header-weight: 400 !important;
vertical-align: middle;
@include no-user-select();
cursor: default;
}
&__left,
&__right {
flex: 1 0 0;
display: flex;
align-items: center;
}
.page-header__dismiss {
width: ($chronograf-page-header-height - 20px);
height: ($chronograf-page-header-height - 20px);
position: relative;
> *:only-child {
margin: 0;
/* Use psuedo elements to render the X */
&: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);
}
&__left {
justify-content: flex-start;
> * {
margin: 0 4px 0 0;
&:after {
transform: translate(-50%,-50%) rotate(-45deg);
}
/* Hover State */
&:hover {
cursor: pointer;
}
&__right {
justify-content: flex-end;
> * {
margin: 0 0 0 4px;
}
}
&.full-width .page-header__container {
max-width: 100%;
}
&.full-width-no-scrollbar {
padding-right: $page-wrapper-padding;
.page-header__container {
max-width: 100%;
}
&:hover:before,
&:hover:after {
background-color: $g18-cloud;
}
}

View File

@ -33,12 +33,35 @@ $dash-graph-options-arrow: 8px;
Default Dashboard Mode
------------------------------------------------------
*/
.dashboard {
.react-grid-item {
.cell-shell {
background-color: $g3-castle;
border-radius: $radius;
border: 2px solid $g3-castle;
transition-property: left, top, border-color, background-color;
}
.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 {
@extend .cell-shell;
}
.graph-empty {
background-color: transparent;
@ -82,6 +105,7 @@ $dash-graph-options-arrow: 8px;
top: $dash-graph-heading;
left: 0;
padding: 0;
z-index: 0;
& > div:not(.graph-empty) {
position: absolute;
@ -353,116 +377,13 @@ $dash-graph-options-arrow: 8px;
}
/*
Cell Edit Mode
Overylay Technology (Cell Edit Mode)
------------------------------------------------------
*/
$overlay-controls-height: 50px;
$overlay-controls-bg: $g2-kevlar;
$overlay-bg: rgba($c-pool, 0.7);
@import 'overlay-technology';
.overlay-technology {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: $overlay-bg;
.overlay-controls {
padding: 0 16px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
flex: 0 0 $overlay-controls-height;
width: calc(100% - #{($explorer-page-padding * 2)});
left: $explorer-page-padding;
margin-top: 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;
}
/*
Template Variables Manager
------------------------------------------------------
*/
@import '../components/template-variables-manager';

View File

@ -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;
}

View File

@ -1,11 +1,12 @@
import AJAX from 'utils/ajax'
export const proxy = async ({source, query, db, rp}) => {
export const proxy = async ({source, query, db, rp, tempVars}) => {
try {
return await AJAX({
method: 'POST',
url: source,
data: {
tempVars,
query,
db,
rp,

View File

@ -4357,6 +4357,10 @@ lodash.merge@^4.4.0:
version "4.6.0"
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:
version "4.4.0"
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-pack "~3.3.0"
node-sass@^3.5.3:
version "3.13.1"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.1.tgz#7240fbbff2396304b4223527ed3020589c004fc2"
node-sass@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64"
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@ -4797,13 +4801,15 @@ node-sass@^3.5.3:
in-publish "^2.0.0"
lodash.assign "^4.2.0"
lodash.clonedeep "^4.3.2"
lodash.mergewith "^4.6.0"
meow "^3.7.0"
mkdirp "^0.5.1"
nan "^2.3.2"
node-gyp "^3.3.1"
npmlog "^4.0.0"
request "^2.61.0"
request "^2.79.0"
sass-graph "^2.1.1"
stdout-stream "^1.4.0"
node-uuid@^1.4.7:
version "1.4.7"
@ -6157,7 +6163,7 @@ request-progress@~2.0.1:
dependencies:
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"
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
dependencies:
@ -6676,6 +6682,12 @@ sshpk@^1.7.0:
version "1.3.1"
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:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"