Merge branch 'master' into file-drag-polish

pull/1897/head
Alex P 2017-08-17 13:35:05 -07:00
commit 052e041a66
90 changed files with 2192 additions and 1956 deletions

View File

@ -4,6 +4,7 @@
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
1. [#1870](https://github.com/influxdata/chronograf/pull/1870): Fix console error for placing prop on div
1. [#1864](https://github.com/influxdata/chronograf/pull/1864): Fix Write Data form upload button and add `onDragExit` handler
1. [#1891](https://github.com/influxdata/chronograf/pull/1891): Fix Kapacitor config for PagerDuty via the UI
### Features
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
@ -19,6 +20,7 @@
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix non-persistence of dashboard graph types
### Features
1. [#1859](https://github.com/influxdata/chronograf/pull/1859): Add y-axis controls to the API for layouts
### UI Improvements
1. [#1846](https://github.com/influxdata/chronograf/pull/1846): Increase screen real estate of Query Maker in the Cell Editor Overlay
@ -30,6 +32,7 @@
1. [#1813](https://github.com/influxdata/chronograf/pull/1813): Guarantee UUID for each Alert Table key to prevent dropping items when keys overlap
### Features
1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis bounds
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis label

View File

@ -98,6 +98,14 @@ func MarshalLayout(l chronograf.Layout) ([]byte, error) {
}
}
axes := make(map[string]*Axis, len(c.Axes))
for a, r := range c.Axes {
axes[a] = &Axis{
Bounds: r.Bounds,
Label: r.Label,
}
}
cells[i] = &Cell{
X: c.X,
Y: c.Y,
@ -107,6 +115,7 @@ func MarshalLayout(l chronograf.Layout) ([]byte, error) {
Name: c.Name,
Queries: queries,
Type: c.Type,
Axes: axes,
}
}
return proto.Marshal(&Layout{
@ -148,6 +157,13 @@ func UnmarshalLayout(data []byte, l *chronograf.Layout) error {
}
}
}
axes := make(map[string]chronograf.Axis, len(c.Axes))
for a, r := range c.Axes {
axes[a] = chronograf.Axis{
Bounds: r.Bounds,
Label: r.Label,
}
}
cells[i] = chronograf.Cell{
X: c.X,
@ -158,6 +174,7 @@ func UnmarshalLayout(data []byte, l *chronograf.Layout) error {
Name: c.Name,
Queries: queries,
Type: c.Type,
Axes: axes,
}
}
l.Cells = cells

View File

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

View File

@ -93,6 +93,7 @@ message Cell {
repeated int64 yranges = 8; // Limits of the y-axes
repeated string ylabels = 9; // Labels of the y-axes
string type = 10; // Cell visualization type
map<string, Axis> axes = 11; // Axes represent the graphical viewport for a cell's visualizations
}
message Query {

View File

@ -108,6 +108,12 @@ func TestMarshalLayout(t *testing.T) {
I: "anotherid",
Type: "line",
Name: "cell1",
Axes: map[string]chronograf.Axis{
"y": chronograf.Axis{
Bounds: []string{"0", "100"},
Label: "foo",
},
},
Queries: []chronograf.Query{
{
Range: &chronograf.Range{
@ -133,8 +139,8 @@ func TestMarshalLayout(t *testing.T) {
t.Fatal(err)
} else if err := internal.UnmarshalLayout(buf, &vv); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(layout, vv) {
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, layout)
} else if !cmp.Equal(layout, vv) {
t.Fatal("source protobuf copy error: diff:\n", cmp.Diff(layout, vv))
}
}

View File

@ -675,14 +675,15 @@ type DashboardsStore interface {
// Cell is a rectangle and multiple time series queries to visualize.
type Cell struct {
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
I string `json:"i"`
Name string `json:"name"`
Queries []Query `json:"queries"`
Type string `json:"type"`
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
I string `json:"i"`
Name string `json:"name"`
Queries []Query `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
}
// Layout is a collection of Cells for visualization

37
mocks/layouts.go Normal file
View File

@ -0,0 +1,37 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.LayoutStore = &LayoutStore{}
type LayoutStore struct {
AddF func(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error)
AllF func(ctx context.Context) ([]chronograf.Layout, error)
DeleteF func(ctx context.Context, layout chronograf.Layout) error
GetF func(ctx context.Context, id string) (chronograf.Layout, error)
UpdateF func(ctx context.Context, layout chronograf.Layout) error
}
func (s *LayoutStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
return s.AddF(ctx, layout)
}
func (s *LayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
return s.AllF(ctx)
}
func (s *LayoutStore) Delete(ctx context.Context, layout chronograf.Layout) error {
return s.DeleteF(ctx, layout)
}
func (s *LayoutStore) Get(ctx context.Context, id string) (chronograf.Layout, error) {
return s.GetF(ctx, id)
}
func (s *LayoutStore) Update(ctx context.Context, layout chronograf.Layout) error {
return s.UpdateF(ctx, layout)
}

View File

@ -23,6 +23,23 @@ func newLayoutResponse(layout chronograf.Layout) layoutResponse {
httpAPILayouts := "/chronograf/v1/layouts"
href := fmt.Sprintf("%s/%s", httpAPILayouts, layout.ID)
rel := "self"
for idx, cell := range layout.Cells {
axes := []string{"x", "y", "y2"}
if cell.Axes == nil {
layout.Cells[idx].Axes = make(map[string]chronograf.Axis, len(axes))
}
for _, axis := range axes {
if _, found := cell.Axes[axis]; !found {
layout.Cells[idx].Axes[axis] = chronograf.Axis{
Bounds: []string{},
}
}
}
}
return layoutResponse{
Layout: layout,
Link: link{

View File

@ -1,79 +1,183 @@
package server_test
/*
func TestNewLayout(t *testing.T) {
t.Parallel()
var tests = []struct {
Desc string
AddError error
ExistingLayouts map[string]chronograf.Layout
NewLayout *models.Layout
ExpectedID int
ExpectedHref string
ExpectedStatus int
import (
"context"
"encoding/json"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/server"
)
func Test_Layouts(t *testing.T) {
layoutTests := []struct {
name string
expected chronograf.Layout
allLayouts []chronograf.Layout
focusedApp string // should filter all layouts to this app only
shouldErr bool
}{
{
Desc: "Test that an error in datastore returns 500 status",
AddError: errors.New("error"),
NewLayout: &models.Layout{
Measurement: new(string),
App: new(string),
Cells: []*models.Cell{
&models.Cell{
X: new(int32),
Y: new(int32),
W: new(int32),
H: new(int32),
},
},
},
ExpectedStatus: http.StatusInternalServerError,
"empty layout",
chronograf.Layout{},
[]chronograf.Layout{},
"",
false,
},
{
Desc: "Test that creating a layout returns 201 status",
ExistingLayouts: map[string]chronograf.Layout{},
NewLayout: &models.Layout{
Measurement: new(string),
App: new(string),
Cells: []*models.Cell{
&models.Cell{
X: new(int32),
Y: new(int32),
W: new(int32),
H: new(int32),
"several layouts",
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
},
[]chronograf.Layout{
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
},
},
"",
false,
},
{
"filtered app",
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
},
[]chronograf.Layout{
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
},
chronograf.Layout{
ID: "b020101b-ea6b-4c8c-9f0e-db0ba501f4ef",
Application: "chronograf",
Measurement: "chronograf",
},
},
"influxdb",
false,
},
{
"axis zero values",
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
Cells: []chronograf.Cell{
{
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
Name: "A Graph",
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
},
"y": chronograf.Axis{
Bounds: []string{},
},
"y2": chronograf.Axis{
Bounds: []string{},
},
},
},
},
},
ExpectedID: 0,
ExpectedHref: "/chronograf/v1/layouts/0",
ExpectedStatus: http.StatusCreated,
[]chronograf.Layout{
chronograf.Layout{
ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c",
Application: "influxdb",
Measurement: "influxdb",
Cells: []chronograf.Cell{
{
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
Name: "A Graph",
},
},
},
},
"",
false,
},
}
for _, test := range tests {
// The mocked backing store will be used to
// check stored values.
store := server.Store{
LayoutStore: &mock.LayoutStore{
AddError: test.AddError,
Layouts: test.ExistingLayouts,
},
}
for _, test := range layoutTests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Send the test layout to the mocked store.
params := op.PostLayoutsParams{
Layout: test.NewLayout,
}
resp := store.NewLayout(context.Background(), params)
w := httptest.NewRecorder()
resp.WriteResponse(w, runtime.JSONProducer())
if w.Code != test.ExpectedStatus {
t.Fatalf("Expected status %d; actual %d", test.ExpectedStatus, w.Code)
}
loc := w.Header().Get("Location")
if loc != test.ExpectedHref {
t.Fatalf("Expected status %s; actual %s", test.ExpectedHref, loc)
}
// setup mock chronograf.Service and mock logger
lg := &mocks.TestLogger{}
svc := server.Service{
LayoutStore: &mocks.LayoutStore{
AllF: func(ctx context.Context) ([]chronograf.Layout, error) {
if len(test.allLayouts) == 0 {
return []chronograf.Layout{
test.expected,
}, nil
} else {
return test.allLayouts, nil
}
},
},
Logger: lg,
}
// setup mock request and response
rr := httptest.NewRecorder()
reqURL := url.URL{
Path: "/chronograf/v1/layouts",
}
params := reqURL.Query()
// add query params required by test
if test.focusedApp != "" {
params.Add("app", test.focusedApp)
}
// re-inject query params
reqURL.RawQuery = params.Encode()
req := httptest.NewRequest("GET", reqURL.RequestURI(), strings.NewReader(""))
// invoke handler for layouts endpoint
svc.Layouts(rr, req)
// create a throwaway frame to unwrap Layouts
respFrame := struct {
Layouts []struct {
chronograf.Layout
Link interface{} `json:"-"`
} `json:"layouts"`
}{}
// decode resp into respFrame
resp := rr.Result()
if err := json.NewDecoder(resp.Body).Decode(&respFrame); err != nil {
t.Fatalf("%q - Error unmarshaling JSON: err: %s", test.name, err.Error())
}
// compare actual and expected
if !cmp.Equal(test.expected, respFrame.Layouts[0].Layout) {
t.Fatalf("%q - Expected layouts to be equal: diff:\n\t%s", test.name, cmp.Diff(test.expected, respFrame.Layouts[0].Layout))
}
})
}
}
*/

View File

@ -3,6 +3,7 @@
plugins: [
'react',
'prettier',
'babel',
],
env: {
browser: true,
@ -98,7 +99,6 @@
'no-floating-decimal': 2,
'no-implicit-coercion': 0,
'no-implied-eval': 2,
'no-invalid-this': 2,
'no-iterator': 2,
'no-lone-blocks': 2,
'no-loop-func': 2,
@ -209,7 +209,7 @@
// React
'jsx-quotes': [1, "prefer-double"],
'react/display-name': 0,
'react/jsx-no-bind': 0,
'react/jsx-no-bind': [2, {ignoreRefs: true}],
'react/jsx-boolean-value': [2, 'always'],
'react/jsx-curly-spacing': [2, 'never'],
'react/jsx-equals-spacing': [2, 'never'],
@ -235,11 +235,16 @@
'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
// Prettier
'prettier/prettier': ['error', {
'singleQuote': true,
'trailingComma': 'es5',
'bracketSpacing': false,
'semi': false,
}],
// Babel
'babel/no-invalid-this': 1
},
}

View File

@ -100,6 +100,7 @@
"calculate-size": "^1.1.1",
"classnames": "^2.2.3",
"dygraphs": "^2.0.0",
"eslint-plugin-babel": "^4.1.2",
"fast.js": "^0.1.1",
"fixed-data-table": "^0.6.1",
"he": "^1.1.1",

View File

@ -12,15 +12,6 @@ class DatabaseRow extends Component {
isEditing: false,
isDeleting: false,
}
this.handleKeyDown = ::this.handleKeyDown
this.handleClickOutside = ::this.handleClickOutside
this.handleStartEdit = ::this.handleStartEdit
this.handleEndEdit = ::this.handleEndEdit
this.handleCreate = ::this.handleCreate
this.handleUpdate = ::this.handleUpdate
this.getInputValues = ::this.getInputValues
this.handleStartDelete = ::this.handleStartDelete
this.handleEndDelete = ::this.handleEndDelete
}
componentWillMount() {
@ -29,9 +20,112 @@ class DatabaseRow extends Component {
}
}
handleRemove = () => {
const {database, retentionPolicy, onRemove} = this.props
onRemove(database, retentionPolicy)
}
handleClickOutside = () => {
const {database, retentionPolicy, onRemove} = this.props
if (retentionPolicy.isNew) {
onRemove(database, retentionPolicy)
}
this.handleEndEdit()
this.handleEndDelete()
}
handleStartEdit = () => {
this.setState({isEditing: true})
}
handleEndEdit = () => {
this.setState({isEditing: false})
}
handleStartDelete = () => {
this.setState({isDeleting: true})
}
handleEndDelete = () => {
this.setState({isDeleting: false})
}
handleCreate = () => {
const {database, retentionPolicy, onCreate} = this.props
const validInputs = this.getInputValues()
if (!validInputs) {
return
}
onCreate(database, {...retentionPolicy, ...validInputs})
this.handleEndEdit()
}
handleUpdate = () => {
const {database, retentionPolicy, onUpdate} = this.props
const validInputs = this.getInputValues()
if (!validInputs) {
return
}
onUpdate(database, retentionPolicy, validInputs)
this.handleEndEdit()
}
handleKeyDown = e => {
const {key} = e
const {retentionPolicy, database, onRemove} = this.props
if (key === 'Escape') {
if (retentionPolicy.isNew) {
onRemove(database, retentionPolicy)
return
}
this.handleEndEdit()
}
if (key === 'Enter') {
if (retentionPolicy.isNew) {
this.handleCreate()
return
}
this.handleUpdate()
}
}
getInputValues = () => {
const {
notify,
retentionPolicy: {name: currentName},
isRFDisplayed,
} = this.props
const name = (this.name && this.name.value.trim()) || currentName
let duration = this.duration.value.trim()
// Replication > 1 is only valid for Influx Enterprise
const replication = isRFDisplayed ? +this.replication.value.trim() : 1
if (!duration || (isRFDisplayed && !replication)) {
notify('error', 'Fields cannot be empty')
return
}
if (duration === '∞') {
duration = 'INF'
}
return {
name,
duration,
replication,
}
}
render() {
const {
onRemove,
retentionPolicy: {name, duration, replication, isDefault, isNew},
retentionPolicy,
database,
@ -53,7 +147,7 @@ class DatabaseRow extends Component {
type="text"
defaultValue={name}
placeholder="Name this RP"
onKeyDown={e => this.handleKeyDown(e, database)}
onKeyDown={this.handleKeyDown}
ref={r => (this.name = r)}
autoFocus={true}
spellCheck={false}
@ -68,7 +162,7 @@ class DatabaseRow extends Component {
type="text"
defaultValue={formattedDuration}
placeholder="How long should Data last"
onKeyDown={e => this.handleKeyDown(e, database)}
onKeyDown={this.handleKeyDown}
ref={r => (this.duration = r)}
autoFocus={!isNew}
spellCheck={false}
@ -84,7 +178,7 @@ class DatabaseRow extends Component {
min="1"
defaultValue={replication || 1}
placeholder="# of Nodes"
onKeyDown={e => this.handleKeyDown(e, database)}
onKeyDown={this.handleKeyDown}
ref={r => (this.replication = r)}
spellCheck={false}
autoComplete={false}
@ -98,11 +192,7 @@ class DatabaseRow extends Component {
<YesNoButtons
buttonSize="btn-xs"
onConfirm={isNew ? this.handleCreate : this.handleUpdate}
onCancel={
isNew
? () => onRemove(database, retentionPolicy)
: this.handleEndEdit
}
onCancel={isNew ? this.handleRemove : this.handleEndEdit}
/>
</td>
</tr>
@ -137,7 +227,7 @@ class DatabaseRow extends Component {
>
{isDeleting
? <YesNoButtons
onConfirm={() => onDelete(database, retentionPolicy)}
onConfirm={onDelete(database, retentionPolicy)}
onCancel={this.handleEndDelete}
buttonSize="btn-xs"
/>
@ -152,105 +242,6 @@ class DatabaseRow extends Component {
</tr>
)
}
handleClickOutside() {
const {database, retentionPolicy, onRemove} = this.props
if (retentionPolicy.isNew) {
onRemove(database, retentionPolicy)
}
this.handleEndEdit()
this.handleEndDelete()
}
handleStartEdit() {
this.setState({isEditing: true})
}
handleEndEdit() {
this.setState({isEditing: false})
}
handleStartDelete() {
this.setState({isDeleting: true})
}
handleEndDelete() {
this.setState({isDeleting: false})
}
handleCreate() {
const {database, retentionPolicy, onCreate} = this.props
const validInputs = this.getInputValues()
if (!validInputs) {
return
}
onCreate(database, {...retentionPolicy, ...validInputs})
this.handleEndEdit()
}
handleUpdate() {
const {database, retentionPolicy, onUpdate} = this.props
const validInputs = this.getInputValues()
if (!validInputs) {
return
}
onUpdate(database, retentionPolicy, validInputs)
this.handleEndEdit()
}
handleKeyDown(e) {
const {key} = e
const {retentionPolicy, database, onRemove} = this.props
if (key === 'Escape') {
if (retentionPolicy.isNew) {
onRemove(database, retentionPolicy)
return
}
this.handleEndEdit()
}
if (key === 'Enter') {
if (retentionPolicy.isNew) {
this.handleCreate()
return
}
this.handleUpdate()
}
}
getInputValues() {
const {
notify,
retentionPolicy: {name: currentName},
isRFDisplayed,
} = this.props
const name = (this.name && this.name.value.trim()) || currentName
let duration = this.duration.value.trim()
// Replication > 1 is only valid for Influx Enterprise
const replication = isRFDisplayed ? +this.replication.value.trim() : 1
if (!duration || (isRFDisplayed && !replication)) {
notify('error', 'Fields cannot be empty')
return
}
if (duration === '∞') {
duration = 'INF'
}
return {
name,
duration,
replication,
}
}
}
const {bool, func, number, shape, string} = PropTypes

View File

@ -57,7 +57,7 @@ const Header = ({
<button
className="btn btn-xs btn-primary"
disabled={isAddRPDisabled}
onClick={() => onAddRetentionPolicy(database)}
onClick={onAddRetentionPolicy(database)}
>
Add Retention Policy
</button>
@ -65,7 +65,7 @@ const Header = ({
? null
: <button
className="btn btn-xs btn-danger"
onClick={() => onStartDelete(database)}
onClick={onStartDelete(database)}
>
Delete
</button>}
@ -88,8 +88,8 @@ const Header = ({
type="text"
value={database.deleteCode || ''}
placeholder={`DELETE ${database.name}`}
onChange={e => onDatabaseDeleteConfirm(database, e)}
onKeyDown={e => onDatabaseDeleteConfirm(database, e)}
onChange={onDatabaseDeleteConfirm(database)}
onKeyDown={onDatabaseDeleteConfirm(database)}
autoFocus={true}
autoComplete={false}
spellCheck={false}
@ -121,8 +121,8 @@ const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) =>
type="text"
value={database.name}
placeholder="Name this Database"
onChange={e => onEdit(database, {name: e.target.value})}
onKeyDown={e => onKeyDown(e, database)}
onChange={onEdit(database)}
onKeyDown={onKeyDown(database)}
autoFocus={true}
spellCheck={false}
autoComplete={false}

View File

@ -3,14 +3,13 @@ import React, {Component, PropTypes} from 'react'
class FilterBar extends Component {
constructor(props) {
super(props)
this.state = {
filterText: '',
}
this.handleText = ::this.handleText
}
handleText(e) {
handleText = e => {
this.setState(
{filterText: e.target.value},
this.props.onFilter(e.target.value)
@ -43,7 +42,7 @@ class FilterBar extends Component {
<button
className="btn btn-sm btn-primary"
disabled={isEditing}
onClick={() => onClickCreate(type)}
onClick={onClickCreate(type)}
>
Create {placeholderText.substring(0, placeholderText.length - 1)}
</button>

View File

@ -43,21 +43,6 @@ const isValidRole = role => {
class AdminPage extends Component {
constructor(props) {
super(props)
this.handleClickCreate = ::this.handleClickCreate
this.handleEditUser = ::this.handleEditUser
this.handleEditRole = ::this.handleEditRole
this.handleSaveUser = ::this.handleSaveUser
this.handleSaveRole = ::this.handleSaveRole
this.handleCancelEditUser = ::this.handleCancelEditUser
this.handleCancelEditRole = ::this.handleCancelEditRole
this.handleDeleteRole = ::this.handleDeleteRole
this.handleDeleteUser = ::this.handleDeleteUser
this.handleUpdateRoleUsers = ::this.handleUpdateRoleUsers
this.handleUpdateRolePermissions = ::this.handleUpdateRolePermissions
this.handleUpdateUserPermissions = ::this.handleUpdateUserPermissions
this.handleUpdateUserRoles = ::this.handleUpdateUserRoles
this.handleUpdateUserPassword = ::this.handleUpdateUserPassword
}
componentDidMount() {
@ -70,7 +55,7 @@ class AdminPage extends Component {
}
}
handleClickCreate(type) {
handleClickCreate = type => () => {
if (type === 'users') {
this.props.addUser()
} else if (type === 'roles') {
@ -78,15 +63,15 @@ class AdminPage extends Component {
}
}
handleEditUser(user, updates) {
handleEditUser = (user, updates) => {
this.props.editUser(user, updates)
}
handleEditRole(role, updates) {
handleEditRole = (role, updates) => {
this.props.editRole(role, updates)
}
async handleSaveUser(user) {
handleSaveUser = async user => {
const {notify} = this.props
if (!isValidUser(user)) {
notify('error', 'Username and/or password too short')
@ -99,7 +84,7 @@ class AdminPage extends Component {
}
}
async handleSaveRole(role) {
handleSaveRole = async role => {
const {notify} = this.props
if (!isValidRole(role)) {
notify('error', 'Role name too short')
@ -112,39 +97,39 @@ class AdminPage extends Component {
}
}
handleCancelEditUser(user) {
handleCancelEditUser = user => {
this.props.removeUser(user)
}
handleCancelEditRole(role) {
handleCancelEditRole = role => {
this.props.removeRole(role)
}
handleDeleteRole(role) {
handleDeleteRole = role => {
this.props.deleteRole(role)
}
handleDeleteUser(user) {
handleDeleteUser = user => {
this.props.deleteUser(user)
}
handleUpdateRoleUsers(role, users) {
handleUpdateRoleUsers = (role, users) => {
this.props.updateRoleUsers(role, users)
}
handleUpdateRolePermissions(role, permissions) {
handleUpdateRolePermissions = (role, permissions) => {
this.props.updateRolePermissions(role, permissions)
}
handleUpdateUserPermissions(user, permissions) {
handleUpdateUserPermissions = (user, permissions) => {
this.props.updateUserPermissions(user, permissions)
}
handleUpdateUserRoles(user, roles) {
handleUpdateUserRoles = (user, roles) => {
this.props.updateUserRoles(user, roles)
}
handleUpdateUserPassword(user, password) {
handleUpdateUserPassword = (user, password) => {
this.props.updateUserPassword(user, password)
}

View File

@ -10,9 +10,6 @@ import {publishAutoDismissingNotification} from 'shared/dispatchers'
class DatabaseManagerPage extends Component {
constructor(props) {
super(props)
this.handleKeyDownDatabase = ::this.handleKeyDownDatabase
this.handleDatabaseDeleteConfirm = ::this.handleDatabaseDeleteConfirm
this.handleCreateDatabase = ::this.handleCreateDatabase
}
componentDidMount() {
@ -32,22 +29,34 @@ class DatabaseManagerPage extends Component {
onKeyDownDatabase={this.handleKeyDownDatabase}
onDatabaseDeleteConfirm={this.handleDatabaseDeleteConfirm}
addDatabase={actions.addDatabase}
onEditDatabase={actions.editDatabase}
onEditDatabase={this.handleEditDatabase}
onCancelDatabase={actions.removeDatabase}
onConfirmDatabase={this.handleCreateDatabase}
onDeleteDatabase={actions.deleteDatabaseAsync}
onStartDeleteDatabase={actions.addDatabaseDeleteCode}
onStartDeleteDatabase={this.handleStartDeleteDatabase}
onRemoveDeleteCode={actions.removeDatabaseDeleteCode}
onAddRetentionPolicy={actions.addRetentionPolicy}
onAddRetentionPolicy={this.handleAddRetentionPolicy}
onCreateRetentionPolicy={actions.createRetentionPolicyAsync}
onUpdateRetentionPolicy={actions.updateRetentionPolicyAsync}
onRemoveRetentionPolicy={actions.removeRetentionPolicy}
onDeleteRetentionPolicy={actions.deleteRetentionPolicyAsync}
onDeleteRetentionPolicy={this.handleDeleteRetentionPolicy}
/>
)
}
handleCreateDatabase(database) {
handleDeleteRetentionPolicy = (db, rp) => () => {
this.props.actions.deleteRetentionPolicyAsync(db, rp)
}
handleStartDeleteDatabase = database => () => {
this.props.actions.addDatabaseDeleteCode(database)
}
handleEditDatabase = database => e => {
this.props.actions.editDatabase(database, {name: e.target.value})
}
handleCreateDatabase = database => {
const {actions, notify, source} = this.props
if (!database.name) {
@ -57,7 +66,12 @@ class DatabaseManagerPage extends Component {
actions.createDatabaseAsync(source.links.databases, database)
}
handleKeyDownDatabase(e, database) {
handleAddRetentionPolicy = database => () => {
const {addRetentionPolicy} = this.props.actions
addRetentionPolicy(database)
}
handleKeyDownDatabase = database => e => {
const {key} = e
const {actions, notify, source} = this.props
@ -74,7 +88,7 @@ class DatabaseManagerPage extends Component {
}
}
handleDatabaseDeleteConfirm(database, e) {
handleDatabaseDeleteConfirm = database => e => {
const {key, target: {value}} = e
const {actions, notify} = this.props

View File

@ -19,18 +19,13 @@ class AlertsTable extends Component {
sortDirection: null,
sortKey: null,
}
this.filterAlerts = ::this.filterAlerts
this.changeSort = ::this.changeSort
this.sortableClasses = ::this.sortableClasses
this.sort = ::this.sort
}
componentWillReceiveProps(newProps) {
this.filterAlerts(this.state.searchTerm, newProps.alerts)
}
filterAlerts(searchTerm, newAlerts) {
filterAlerts = (searchTerm, newAlerts) => {
const alerts = newAlerts || this.props.alerts
const filterText = searchTerm.toLowerCase()
const filteredAlerts = alerts.filter(h => {
@ -47,7 +42,7 @@ class AlertsTable extends Component {
this.setState({searchTerm, filteredAlerts})
}
changeSort(key) {
changeSort = key => () => {
// if we're using the key, reverse order; otherwise, set it with ascending
if (this.state.sortKey === key) {
const reverseDirection =
@ -58,7 +53,7 @@ class AlertsTable extends Component {
}
}
sortableClasses(key) {
sortableClasses = key => () => {
if (this.state.sortKey === key) {
if (this.state.sortDirection === 'asc') {
return 'alert-history-table--th sortable-header sorting-ascending'
@ -68,7 +63,7 @@ class AlertsTable extends Component {
return 'alert-history-table--th sortable-header'
}
sort(alerts, key, direction) {
sort = (alerts, key, direction) => {
switch (direction) {
case 'asc':
return _.sortBy(alerts, e => e[key])
@ -91,35 +86,35 @@ class AlertsTable extends Component {
? <div className="alert-history-table">
<div className="alert-history-table--thead">
<div
onClick={() => this.changeSort('name')}
onClick={this.changeSort('name')}
className={this.sortableClasses('name')}
style={{width: colName}}
>
Name
</div>
<div
onClick={() => this.changeSort('level')}
onClick={this.changeSort('level')}
className={this.sortableClasses('level')}
style={{width: colLevel}}
>
Level
</div>
<div
onClick={() => this.changeSort('time')}
onClick={this.changeSort('time')}
className={this.sortableClasses('time')}
style={{width: colTime}}
>
Time
</div>
<div
onClick={() => this.changeSort('host')}
onClick={this.changeSort('host')}
className={this.sortableClasses('host')}
style={{width: colHost}}
>
Host
</div>
<div
onClick={() => this.changeSort('value')}
onClick={this.changeSort('value')}
className={this.sortableClasses('value')}
style={{width: colValue}}
>

View File

@ -23,19 +23,6 @@ class CellEditorOverlay extends Component {
constructor(props) {
super(props)
this.queryStateReducer = ::this.queryStateReducer
this.handleAddQuery = ::this.handleAddQuery
this.handleDeleteQuery = ::this.handleDeleteQuery
this.handleSaveCell = ::this.handleSaveCell
this.handleSelectGraphType = ::this.handleSelectGraphType
this.handleClickDisplayOptionsTab = ::this.handleClickDisplayOptionsTab
this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
this.handleEditRawText = ::this.handleEditRawText
this.handleSetYAxisBoundMin = ::this.handleSetYAxisBoundMin
this.handleSetYAxisBoundMax = ::this.handleSetYAxisBoundMax
this.handleSetLabel = ::this.handleSetLabel
this.getActiveQuery = ::this.getActiveQuery
const {cell: {name, type, queries, axes}} = props
const queriesWorkingDraft = _.cloneDeep(
@ -65,21 +52,19 @@ class CellEditorOverlay extends Component {
}
}
queryStateReducer(queryModifier) {
return (queryID, payload) => {
const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find(q => q.id === queryID)
queryStateReducer = queryModifier => (queryID, payload) => {
const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find(q => q.id === queryID)
const nextQuery = queryModifier(query, payload)
const nextQuery = queryModifier(query, payload)
const nextQueries = queriesWorkingDraft.map(
q => (q.id === query.id ? nextQuery : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
const nextQueries = queriesWorkingDraft.map(
q => (q.id === query.id ? nextQuery : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
handleSetYAxisBoundMin(min) {
handleSetYAxisBoundMin = min => {
const {axes} = this.state
const {y: {bounds: [, max]}} = axes
@ -88,7 +73,7 @@ class CellEditorOverlay extends Component {
})
}
handleSetYAxisBoundMax(max) {
handleSetYAxisBoundMax = max => {
const {axes} = this.state
const {y: {bounds: [min]}} = axes
@ -97,13 +82,13 @@ class CellEditorOverlay extends Component {
})
}
handleSetLabel(label) {
handleSetLabel = label => {
const {axes} = this.state
this.setState({axes: {...axes, y: {...axes.y, label}}})
}
handleAddQuery() {
handleAddQuery = () => {
const {queriesWorkingDraft} = this.state
const newIndex = queriesWorkingDraft.length
@ -116,14 +101,14 @@ class CellEditorOverlay extends Component {
this.handleSetActiveQueryIndex(newIndex)
}
handleDeleteQuery(index) {
handleDeleteQuery = index => {
const nextQueries = this.state.queriesWorkingDraft.filter(
(__, i) => i !== index
)
this.setState({queriesWorkingDraft: nextQueries})
}
handleSaveCell() {
handleSaveCell = () => {
const {
queriesWorkingDraft,
cellWorkingType: type,
@ -152,21 +137,19 @@ class CellEditorOverlay extends Component {
})
}
handleSelectGraphType(graphType) {
handleSelectGraphType = graphType => () => {
this.setState({cellWorkingType: graphType})
}
handleClickDisplayOptionsTab(isDisplayOptionsTabActive) {
return () => {
this.setState({isDisplayOptionsTabActive})
}
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
this.setState({isDisplayOptionsTabActive})
}
handleSetActiveQueryIndex(activeQueryIndex) {
handleSetActiveQueryIndex = activeQueryIndex => {
this.setState({activeQueryIndex})
}
getActiveQuery() {
getActiveQuery = () => {
const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = queriesWorkingDraft[activeQueryIndex]
const defaultQuery = queriesWorkingDraft[0]
@ -174,7 +157,7 @@ class CellEditorOverlay extends Component {
return activeQuery || defaultQuery
}
async handleEditRawText(url, id, text) {
handleEditRawText = async (url, id, text) => {
const templates = removeUnselectedTemplateValues(this.props.templates)
// use this as the handler passed into fetchTimeSeries to update a query status

View File

@ -6,23 +6,22 @@ class DashboardEditHeader extends Component {
super(props)
const {dashboard: {name}} = props
this.state = {name}
this.handleChange = ::this.handleChange
this.handleFormSubmit = ::this.handleFormSubmit
this.handleKeyUp = ::this.handleKeyUp
this.state = {
name,
}
}
handleChange(name) {
this.setState({name})
handleChange = e => {
this.setState({name: e.target.value})
}
handleFormSubmit(e) {
handleFormSubmit = e => {
e.preventDefault()
const name = e.target.name.value
this.props.onSave(name)
}
handleKeyUp(e) {
handleKeyUp = e => {
const {onCancel} = this.props
if (e.key === 'Escape') {
onCancel()
@ -46,11 +45,11 @@ class DashboardEditHeader extends Component {
name="name"
value={name}
placeholder="Name this Dashboard"
onChange={e => this.handleChange(e.target.value)}
onKeyUp={this.handleKeyUp}
autoFocus={true}
spellCheck={false}
autoComplete="off"
onChange={this.handleChange}
/>
</form>
<ConfirmButtons item={name} onConfirm={onSave} onCancel={onCancel} />

View File

@ -14,7 +14,7 @@ const GraphTypeSelector = ({selectedGraphType, onSelectGraphType}) =>
active: graphType.type === selectedGraphType,
})}
>
<div onClick={() => onSelectGraphType(graphType.type)}>
<div onClick={onSelectGraphType(graphType.type)}>
{graphType.graphic}
<p>
{graphType.menuOption}

View File

@ -11,8 +11,6 @@ class MeasurementDropdown extends Component {
this.state = {
measurements: [],
}
this._getMeasurements = ::this._getMeasurements
}
componentDidMount() {
@ -35,12 +33,12 @@ class MeasurementDropdown extends Component {
items={measurements.map(text => ({text}))}
selected={measurement || 'Select Measurement'}
onChoose={onSelectMeasurement}
onClick={() => onStartEdit(null)}
onClick={onStartEdit}
/>
)
}
async _getMeasurements() {
_getMeasurements = async () => {
const {source: {links: {proxy}}} = this.context
const {
measurement,

View File

@ -23,17 +23,6 @@ class QueryTextArea extends Component {
},
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) {
@ -42,21 +31,21 @@ class QueryTextArea extends Component {
}
}
handleCloseDrawer() {
handleCloseDrawer = () => {
this.setState({isTemplating: false})
}
handleMouseOverTempVar(template) {
handleMouseOverTempVar = template => () => {
this.handleTemplateReplace(template)
}
handleClickTempVar(template) {
handleClickTempVar = template => () => {
// Clicking a tempVar does the same thing as hitting 'Enter'
this.handleTemplateReplace(template, true)
this.closeDrawer()
}
closeDrawer() {
closeDrawer = () => {
this.setState({
isTemplating: false,
selectedTemplate: {
@ -65,7 +54,7 @@ class QueryTextArea extends Component {
})
}
handleKeyDown(e) {
handleKeyDown = e => {
const {isTemplating, value} = this.state
if (isTemplating) {
@ -96,7 +85,7 @@ class QueryTextArea extends Component {
}
}
handleTemplateReplace(selectedTemplate, replaceWholeTemplate) {
handleTemplateReplace = (selectedTemplate, replaceWholeTemplate) => {
const {selectionStart, value} = this.editor
const {tempVar} = selectedTemplate
const newTempVar = replaceWholeTemplate
@ -125,7 +114,7 @@ class QueryTextArea extends Component {
)
}
findTempVar(direction) {
findTempVar = direction => {
const {filteredTemplates: templates} = this.state
const {selectedTemplate} = this.state
@ -149,7 +138,7 @@ class QueryTextArea extends Component {
return templates[0]
}
handleChange() {
handleChange = () => {
const {templates} = this.props
const {selectedTemplate} = this.state
const value = this.editor.value
@ -186,14 +175,10 @@ class QueryTextArea extends Component {
}
}
handleUpdate() {
handleUpdate = () => {
this.props.onUpdate(this.state.value)
}
handleChooseTemplate(template) {
this.setState({value: template.query})
}
handleSelectTempVar(tempVar) {
this.setState({selectedTemplate: tempVar})
}

View File

@ -11,8 +11,6 @@ class TagKeyDropdown extends Component {
this.state = {
tagKeys: [],
}
this._getTags = ::this._getTags
}
componentDidMount() {
@ -32,18 +30,20 @@ class TagKeyDropdown extends Component {
render() {
const {tagKeys} = this.state
const {tagKey, onSelectTagKey, onStartEdit} = this.props
const {tagKey, onSelectTagKey} = this.props
return (
<Dropdown
items={tagKeys.map(text => ({text}))}
selected={tagKey || 'Select Tag Key'}
onChoose={onSelectTagKey}
onClick={() => onStartEdit(null)}
onClick={this.handleStartEdit}
/>
)
}
async _getTags() {
handleStartEdit = () => this.props.onStartEdit(null)
_getTags = async () => {
const {
database,
measurement,

View File

@ -4,17 +4,15 @@ import calculateSize from 'calculate-size'
import Dropdown from 'shared/components/Dropdown'
import omit from 'lodash/omit'
const minTempVarDropdownWidth = 146
const maxTempVarDropdownWidth = 300
const tempVarDropdownPadding = 30
const TemplateControlBar = ({
isOpen,
templates,
onSelectTemplate,
onOpenTemplateManager,
isOpen,
}) =>
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
@ -65,8 +63,7 @@ const TemplateControlBar = ({
menuClass="dropdown-astronaut"
useAutoComplete={true}
selected={selectedText || '(No values)'}
onChoose={item =>
onSelectTemplate(id, [item].map(x => omit(x, 'text')))}
onChoose={onSelectTemplate(id)}
/>
<label className="template-control--label">
{tempVar}

View File

@ -2,22 +2,22 @@ import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import uuid from 'node-uuid'
import TemplateVariableTable from 'src/dashboards/components/TemplateVariableTable'
import TemplateVariableTable from 'src/dashboards/components/template_variables/Table'
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
const TemplateVariableManager = ({
onClose,
onEditTemplateVariables,
source,
onClose,
onDelete,
isEdited,
templates,
onAddVariable,
onRunQuerySuccess,
onRunQueryFailure,
onSaveTemplatesSuccess,
onAddVariable,
onDelete,
tempVarAlreadyExists,
isEdited,
onSaveTemplatesSuccess,
onEditTemplateVariables,
}) =>
<div className="template-variable-manager">
<div className="template-variable-manager--header">
@ -41,19 +41,16 @@ const TemplateVariableManager = ({
>
Save Changes
</button>
<span
className="page-header__dismiss"
onClick={() => onClose(isEdited)}
/>
<span className="page-header__dismiss" onClick={onClose(isEdited)} />
</div>
</div>
<div className="template-variable-manager--body">
<TemplateVariableTable
source={source}
onDelete={onDelete}
templates={templates}
onRunQuerySuccess={onRunQuerySuccess}
onRunQueryFailure={onRunQueryFailure}
onDelete={onDelete}
tempVarAlreadyExists={tempVarAlreadyExists}
/>
</div>

View File

@ -8,8 +8,10 @@ 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 TemplateQueryBuilder from 'src/dashboards/components/template_variables/TemplateQueryBuilder'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
import RowValues from 'src/dashboards/components/template_variables/RowValues'
import RowButtons from 'src/dashboards/components/template_variables/RowButtons'
import {runTemplateVariableQuery as runTemplateVariableQueryAJAX} from 'src/dashboards/apis'
@ -23,79 +25,6 @@ import {publishAutoDismissingNotification} from 'shared/dispatchers'
const compact = values => uniq(values).filter(value => /\S/.test(value))
const RowValues = ({
selectedType,
values = [],
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
const _values = values.map(v => v.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 btn-square"
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,
@ -111,8 +40,8 @@ const TemplateVariableRow = ({
onCancelEdit,
autoFocusTarget,
onSubmit,
onDelete,
onErrorThrown,
onDeleteTempVar,
}) =>
<form
className={classnames('template-variable-manager--table-row', {
@ -138,7 +67,7 @@ const TemplateVariableRow = ({
<Dropdown
items={TEMPLATE_TYPES}
onChoose={onSelectType}
onClick={() => onStartEdit(null)}
onClick={onStartEdit}
selected={TEMPLATE_TYPES.find(t => t.type === selectedType).text}
className="dropdown-140"
/>
@ -168,42 +97,13 @@ const TemplateVariableRow = ({
onStartEdit={onStartEdit}
isEditing={isEditing}
onCancelEdit={onCancelEdit}
onDelete={onDelete}
onDelete={onDeleteTempVar}
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)
@ -220,85 +120,74 @@ class RowWrapper extends Component {
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({
handleSubmit = ({
selectedDatabase: database,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
selectedType: type,
}) {
return async e => {
e.preventDefault()
}) => async e => {
e.preventDefault()
const {
source,
template,
template: {id},
onRunQuerySuccess,
onRunQueryFailure,
tempVarAlreadyExists,
notify,
} = this.props
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
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.`
)
}
if (tempVarAlreadyExists(tempVar, id)) {
return notify(
'error',
`Variable '${_tempVar}' already exists. Please enter a new value.`
)
}
this.setState({
isEditing: false,
hasBeenSavedToComponentStateOnce: true,
})
this.setState({
isEditing: false,
hasBeenSavedToComponentStateOnce: true,
})
const {query, tempVars} = generateTemplateVariableQuery({
type,
tempVar,
query: {
database,
// rp, TODO
measurement,
tagKey,
},
})
const queryConfig = {
type,
tempVars,
query,
const {query, tempVars} = generateTemplateVariableQuery({
type,
tempVar,
query: {
database,
// rp: TODO
// 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)
}
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, compact(parsedData), tempVar)
} catch (error) {
onRunQueryFailure(error)
}
onRunQuerySuccess(template, queryConfig, compact(parsedData), tempVar)
} catch (error) {
onRunQueryFailure(error)
}
}
@ -306,11 +195,11 @@ class RowWrapper extends Component {
this.handleCancelEdit()
}
handleStartEdit(name) {
handleStartEdit = name => () => {
this.setState({isEditing: true, autoFocusTarget: name})
}
handleCancelEdit() {
handleCancelEdit = () => {
const {
template: {type, query: {db, measurement, tagKey}, id},
onDelete,
@ -329,7 +218,7 @@ class RowWrapper extends Component {
})
}
handleSelectType(item) {
handleSelectType = item => {
this.setState({
selectedType: item.type,
selectedDatabase: null,
@ -338,22 +227,22 @@ class RowWrapper extends Component {
})
}
handleSelectDatabase(item) {
handleSelectDatabase = item => {
this.setState({selectedDatabase: item.text})
}
handleSelectMeasurement(item) {
handleSelectMeasurement = item => {
this.setState({selectedMeasurement: item.text})
}
handleSelectTagKey(item) {
handleSelectTagKey = item => {
this.setState({selectedTagKey: item.text})
}
async runTemplateVariableQuery(
runTemplateVariableQuery = async (
source,
{query, database, rp, tempVars, type, measurement, tagKey}
) {
) => {
try {
const {data} = await runTemplateVariableQueryAJAX(source, {
query,
@ -373,6 +262,10 @@ class RowWrapper extends Component {
}
}
handleDelete = id => () => {
this.props.onDelete(id)
}
render() {
const {
isEditing,
@ -399,6 +292,7 @@ class RowWrapper extends Component {
onCancelEdit={this.handleCancelEdit}
autoFocusTarget={autoFocusTarget}
onSubmit={this.handleSubmit}
onDeleteTempVar={this.handleDelete}
/>
)
}
@ -453,31 +347,6 @@ TemplateVariableRow.propTypes = {
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),

View File

@ -0,0 +1,53 @@
import React, {PropTypes} from 'react'
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
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 btn-square"
type="button"
onClick={onStartEdit('tempVar')}
>
<span className="icon pencil" />
</button>
</div>
)
}
const {bool, func, string} = PropTypes
RowButtons.propTypes = {
onStartEdit: func.isRequired,
isEditing: bool.isRequired,
onCancelEdit: func.isRequired,
onDelete: func.isRequired,
id: string.isRequired,
selectedType: string.isRequired,
}
export default RowButtons

View File

@ -0,0 +1,43 @@
import React, {PropTypes} from 'react'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
const RowValues = ({
selectedType,
values = [],
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
const _values = values.map(v => v.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 {arrayOf, bool, func, shape, string} = PropTypes
RowValues.propTypes = {
selectedType: string.isRequired,
values: arrayOf(shape()),
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
autoFocusTarget: string,
}
export default RowValues

View File

@ -1,6 +1,6 @@
import React, {PropTypes} from 'react'
import TemplateVariableRow from 'src/dashboards/components/TemplateVariableRow'
import TemplateVariableRow from 'src/dashboards/components/template_variables/Row'
const TemplateVariableTable = ({
source,

View File

@ -0,0 +1,42 @@
import React, {PropTypes} from 'react'
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>
}
const {bool, func, string} = PropTypes
TableInput.propTypes = {
defaultValue: string,
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
name: string.isRequired,
autoFocusTarget: string,
}
export default TableInput

View File

@ -10,7 +10,7 @@ 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 TemplateVariableManager from 'src/dashboards/components/template_variables/Manager'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
@ -27,33 +27,11 @@ class DashboardPage extends Component {
super(props)
this.state = {
selectedCell: null,
isEditMode: false,
isTemplating: false,
dygraphs: [],
isEditMode: false,
selectedCell: null,
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.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
this.handleToggleTempVarControls = ::this.handleToggleTempVarControls
this.synchronizer = ::this.synchronizer
}
async componentDidMount() {
@ -75,11 +53,11 @@ class DashboardPage extends Component {
await putDashboardByID(dashboardID)
}
handleOpenTemplateManager() {
handleOpenTemplateManager = () => {
this.setState({isTemplating: true})
}
handleCloseTemplateManager(isEdited) {
handleCloseTemplateManager = isEdited => () => {
if (
!isEdited ||
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
@ -88,43 +66,43 @@ class DashboardPage extends Component {
}
}
handleDismissOverlay() {
handleDismissOverlay = () => {
this.setState({selectedCell: null})
}
handleSaveEditedCell(newCell) {
handleSaveEditedCell = newCell => {
this.props.dashboardActions
.updateDashboardCell(this.getActiveDashboard(), newCell)
.then(this.handleDismissOverlay)
}
handleSummonOverlayTechnologies(cell) {
handleSummonOverlayTechnologies = cell => {
this.setState({selectedCell: cell})
}
handleChooseTimeRange(timeRange) {
handleChooseTimeRange = timeRange => {
this.props.dashboardActions.setTimeRange(timeRange)
}
handleUpdatePosition(cells) {
handleUpdatePosition = cells => {
const newDashboard = {...this.getActiveDashboard(), cells}
this.props.dashboardActions.updateDashboard(newDashboard)
this.props.dashboardActions.putDashboard(newDashboard)
}
handleAddCell() {
handleAddCell = () => {
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
}
handleEditDashboard() {
handleEditDashboard = () => {
this.setState({isEditMode: true})
}
handleCancelEditDashboard() {
handleCancelEditDashboard = () => {
this.setState({isEditMode: false})
}
handleRenameDashboard(name) {
handleRenameDashboard = name => {
this.setState({isEditMode: false})
const newDashboard = {...this.getActiveDashboard(), name}
this.props.dashboardActions.updateDashboard(newDashboard)
@ -132,7 +110,7 @@ class DashboardPage extends Component {
}
// Places cell into editing mode.
handleEditDashboardCell(x, y, isEditing) {
handleEditDashboardCell = (x, y, isEditing) => {
return () => {
this.props.dashboardActions.editDashboardCell(
this.getActiveDashboard(),
@ -143,7 +121,7 @@ class DashboardPage extends Component {
}
}
handleUpdateDashboardCell(newCell) {
handleUpdateDashboardCell = newCell => {
return () => {
this.props.dashboardActions.updateDashboardCell(
this.getActiveDashboard(),
@ -152,60 +130,41 @@ class DashboardPage extends Component {
}
}
handleDeleteDashboardCell(cell) {
handleDeleteDashboardCell = cell => {
const dashboard = this.getActiveDashboard()
this.props.dashboardActions.deleteDashboardCellAsync(dashboard, cell)
}
handleSelectTemplate(templateID, values) {
handleSelectTemplate = templateID => values => {
const {params: {dashboardID}} = this.props
this.props.dashboardActions.templateVariableSelected(
+dashboardID,
templateID,
values
[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 () => {
try {
await this.props.dashboardActions.putDashboard({
...this.getActiveDashboard(),
templates,
})
onSaveTemplatesSuccess()
} catch (error) {
console.error(error)
}
handleEditTemplateVariables = (
templates,
onSaveTemplatesSuccess
) => async () => {
try {
await this.props.dashboardActions.putDashboard({
...this.getActiveDashboard(),
templates,
})
onSaveTemplatesSuccess()
} catch (error) {
console.error(error)
}
}
handleRunQueryFailure(error) {
handleRunQueryFailure = error => {
console.error(error)
this.props.errorThrown(error)
}
synchronizer(dygraph) {
synchronizer = dygraph => {
const dygraphs = [...this.state.dygraphs, dygraph]
const {dashboards, params} = this.props
const dashboard = dashboards.find(d => d.id === +params.dashboardID)
@ -223,11 +182,11 @@ class DashboardPage extends Component {
this.setState({dygraphs})
}
handleToggleTempVarControls() {
handleToggleTempVarControls = () => {
this.props.templateControlBarVisibilityToggled()
}
handleCancelEditCell(cellID) {
handleCancelEditCell = cellID => {
this.props.dashboardActions.cancelEditCell(
this.getActiveDashboard().id,
cellID
@ -315,49 +274,49 @@ class DashboardPage extends Component {
{isTemplating
? <OverlayTechnologies>
<TemplateVariableManager
onClose={this.handleCloseTemplateManager}
onEditTemplateVariables={this.handleEditTemplateVariables}
source={source}
templates={dashboard.templates}
onClose={this.handleCloseTemplateManager}
onRunQueryFailure={this.handleRunQueryFailure}
onEditTemplateVariables={this.handleEditTemplateVariables}
/>
</OverlayTechnologies>
: null}
{selectedCell
? <CellEditorOverlay
source={source}
dashboardID={dashboardID}
templates={templatesIncludingDashTime}
cell={selectedCell}
timeRange={timeRange}
autoRefresh={autoRefresh}
dashboardID={dashboardID}
queryStatus={cellQueryStatus}
onSave={this.handleSaveEditedCell}
onCancel={this.handleDismissOverlay}
templates={templatesIncludingDashTime}
editQueryStatus={dashboardActions.editCellQueryStatus}
/>
: null}
{isEditMode
? <DashboardHeaderEdit
dashboard={dashboard}
onCancel={this.handleCancelEditDashboard}
onSave={this.handleRenameDashboard}
onCancel={this.handleCancelEditDashboard}
/>
: <DashboardHeader
buttonText={dashboard ? dashboard.name : ''}
handleChooseAutoRefresh={handleChooseAutoRefresh}
autoRefresh={autoRefresh}
timeRange={timeRange}
handleChooseTimeRange={this.handleChooseTimeRange}
isHidden={inPresentationMode}
handleClickPresentationButton={handleClickPresentationButton}
dashboard={dashboard}
sourceID={sourceID}
source={source}
sourceID={sourceID}
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
isHidden={inPresentationMode}
onAddCell={this.handleAddCell}
onEditDashboard={this.handleEditDashboard}
onToggleTempVarControls={this.handleToggleTempVarControls}
buttonText={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
handleChooseAutoRefresh={handleChooseAutoRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
onToggleTempVarControls={this.handleToggleTempVarControls}
handleClickPresentationButton={handleClickPresentationButton}
>
{dashboards
? dashboards.map((d, i) =>
@ -375,19 +334,19 @@ class DashboardPage extends Component {
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
synchronizer={this.synchronizer}
onAddCell={this.handleAddCell}
onEditCell={this.handleEditDashboardCell}
synchronizer={this.synchronizer}
inPresentationMode={inPresentationMode}
onEditCell={this.handleEditDashboardCell}
onPositionChange={this.handleUpdatePosition}
onSelectTemplate={this.handleSelectTemplate}
onCancelEditCell={this.handleCancelEditCell}
onDeleteCell={this.handleDeleteDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
showTemplateControlBar={showTemplateControlBar}
onOpenTemplateManager={this.handleOpenTemplateManager}
templatesIncludingDashTime={templatesIncludingDashTime}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
onSelectTemplate={this.handleSelectTemplate}
showTemplateControlBar={showTemplateControlBar}
onCancelEditCell={::this.handleCancelEditCell}
/>
: null}
</div>

View File

@ -0,0 +1,29 @@
import React, {PropTypes} from 'react'
import moment from 'moment'
const CustomCell = ({data, columnName}) => {
if (columnName === 'time') {
const date = moment(new Date(data)).format('MM/DD/YY hh:mm:ssA')
return (
<span>
{date}
</span>
)
}
return (
<span>
{data}
</span>
)
}
const {number, oneOfType, string} = PropTypes
CustomCell.propTypes = {
data: oneOfType([number, string]),
columnName: string.isRequired,
}
export default CustomCell

View File

@ -1,84 +0,0 @@
import React, {PropTypes} from 'react'
import {showDatabases, showRetentionPolicies} from 'shared/apis/metaQuery'
import showDatabasesParser from 'shared/parsing/showDatabases'
import showRetentionPoliciesParser from 'shared/parsing/showRetentionPolicies'
import Dropdown from 'shared/components/Dropdown'
const {func, shape, string} = PropTypes
const DatabaseDropdown = React.createClass({
propTypes: {
query: shape({}).isRequired,
onChooseNamespace: func.isRequired,
},
contextTypes: {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
},
getInitialState() {
return {
namespaces: [],
}
},
componentDidMount() {
const {source} = this.context
const proxy = source.links.proxy
showDatabases(proxy).then(resp => {
const {errors, databases} = showDatabasesParser(resp.data)
if (errors.length) {
// do something
}
const namespaces = []
showRetentionPolicies(proxy, databases).then(res => {
res.data.results.forEach((result, index) => {
const {errors: errs, retentionPolicies} = showRetentionPoliciesParser(
result
)
if (errs.length) {
// do something
}
retentionPolicies.forEach(rp => {
namespaces.push({
database: databases[index],
retentionPolicy: rp.name,
})
})
})
this.setState({namespaces})
})
})
},
render() {
const {query, onChooseNamespace} = this.props
const {namespaces} = this.state
return (
<Dropdown
className="dropdown-160 query-builder--db-dropdown"
items={namespaces.map(n => ({
...n,
text: `${n.database}.${n.retentionPolicy}`,
}))}
onChoose={onChooseNamespace}
selected={
query.database && query.retentionPolicy
? `${query.database}.${query.retentionPolicy}`
: 'Choose a DB & RP'
}
/>
)
},
})
export default DatabaseDropdown

View File

@ -1,75 +1,31 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import Dimensions from 'react-dimensions'
import _ from 'lodash'
import moment from 'moment'
import classnames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {Table, Column, Cell} from 'fixed-data-table'
import Dropdown from 'shared/components/Dropdown'
import CustomCell from 'src/data_explorer/components/CustomCell'
import TabItem from 'src/data_explorer/components/TableTabItem'
const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
const emptySeries = {columns: [], values: []}
const CustomCell = React.createClass({
propTypes: {
data: oneOfType([number, string]),
columnName: string.isRequired,
},
class ChronoTable extends Component {
constructor(props) {
super(props)
render() {
const {columnName, data} = this.props
if (columnName === 'time') {
const date = moment(new Date(data)).format('MM/DD/YY hh:mm:ssA')
return (
<span>
{date}
</span>
)
}
return (
<span>
{data}
</span>
)
},
})
const ChronoTable = React.createClass({
propTypes: {
query: shape({
host: arrayOf(string.isRequired).isRequired,
text: string.isRequired,
id: string.isRequired,
}).isRequired,
containerWidth: number.isRequired,
height: number,
editQueryStatus: func.isRequired,
},
getInitialState() {
return {
this.state = {
series: [emptySeries],
columnWidths: {},
activeSeriesIndex: 0,
}
},
getDefaultProps() {
return {
height: 500,
}
},
}
componentDidMount() {
this.fetchCellData(this.props.query)
},
}
componentWillReceiveProps(nextProps) {
if (this.props.query.text === nextProps.query.text) {
@ -77,9 +33,9 @@ const ChronoTable = React.createClass({
}
this.fetchCellData(nextProps.query)
},
}
async fetchCellData(query) {
fetchCellData = async query => {
if (!query || !query.text) {
return
}
@ -105,9 +61,9 @@ const ChronoTable = React.createClass({
})
throw error
}
},
}
handleColumnResize(newColumnWidth, columnKey) {
handleColumnResize = (newColumnWidth, columnKey) => {
const columnWidths = {
...this.state.columnWidths,
[columnKey]: newColumnWidth,
@ -116,15 +72,21 @@ const ChronoTable = React.createClass({
this.setState({
columnWidths,
})
},
}
handleClickTab(activeSeriesIndex) {
handleClickTab = activeSeriesIndex => () => {
this.setState({activeSeriesIndex})
},
}
handleClickDropdown(item) {
handleClickDropdown = item => {
this.setState({activeSeriesIndex: item.index})
},
}
handleCustomCell = (columnName, values, colIndex) => ({rowIndex}) => {
return (
<CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
)
}
render() {
const {containerWidth, height, query} = this.props
@ -200,11 +162,7 @@ const ChronoTable = React.createClass({
{columnName}
</Cell>
}
cell={({rowIndex}) =>
<CustomCell
columnName={columnName}
data={values[rowIndex][colIndex]}
/>}
cell={this.handleCustomCell(columnName, values, colIndex)}
width={columnWidths[columnName] || width}
minWidth={minWidth}
/>
@ -214,22 +172,24 @@ const ChronoTable = React.createClass({
</div>
</div>
)
},
})
}
}
const TabItem = ({name, index, onClickTab, isActive}) =>
<div
className={classnames('table--tab', {active: isActive})}
onClick={() => onClickTab(index)}
>
{name}
</div>
ChronoTable.defaultProps = {
height: 500,
}
TabItem.propTypes = {
name: string,
onClickTab: func.isRequired,
index: number.isRequired,
isActive: bool,
const {arrayOf, func, number, shape, string} = PropTypes
ChronoTable.propTypes = {
query: shape({
host: arrayOf(string.isRequired).isRequired,
text: string.isRequired,
id: string.isRequired,
}).isRequired,
containerWidth: number.isRequired,
height: number,
editQueryStatus: func.isRequired,
}
export default Dimensions({elementResize: true})(ChronoTable)

View File

@ -0,0 +1,21 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
const TableTabItem = ({name, index, onClickTab, isActive}) =>
<div
className={classnames('table--tab', {active: isActive})}
onClick={onClickTab(index)}
>
{name}
</div>
const {bool, func, number, string} = PropTypes
TableTabItem.propTypes = {
name: string,
onClickTab: func.isRequired,
index: number.isRequired,
isActive: bool,
}
export default TableTabItem

View File

@ -9,7 +9,7 @@ const VisHeader = ({views, view, onToggleView, name}) =>
{views.map(v =>
<li
key={v}
onClick={() => onToggleView(v)}
onClick={onToggleView(v)}
className={classnames({active: view === v})}
data-test={`data-${v}`}
>

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import buildInfluxQLQuery from 'utils/influxql'
import classnames from 'classnames'
import VisHeader from 'src/data_explorer/components/VisHeader'
@ -6,57 +6,19 @@ import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'shared/constants'
import _ from 'lodash'
const {arrayOf, bool, func, number, shape, string} = PropTypes
const META_QUERY_REGEX = /^show/i
const Visualization = React.createClass({
propTypes: {
cellName: string,
cellType: string,
autoRefresh: number.isRequired,
templates: arrayOf(shape()),
isInDataExplorer: bool,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
activeQueryIndex: number,
height: string,
heightPixels: number,
editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
}),
}),
resizerBottomHeight: number,
},
class Visualization extends Component {
constructor(props) {
super(props)
contextTypes: {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
},
getInitialState() {
const {activeQueryIndex, queryConfigs} = this.props
const activeQueryText = this.getQueryText(queryConfigs, activeQueryIndex)
return activeQueryText.match(META_QUERY_REGEX)
this.state = activeQueryText.match(META_QUERY_REGEX)
? {view: TABLE}
: {view: GRAPH}
},
getDefaultProps() {
return {
cellName: '',
cellType: '',
}
},
}
componentWillReceiveProps(nextProps) {
const {activeQueryIndex, queryConfigs} = nextProps
@ -75,11 +37,11 @@ const Visualization = React.createClass({
}
this.setState({view: GRAPH})
},
}
handleToggleView(view) {
handleToggleView = view => () => {
this.setState({view})
},
}
render() {
const {
@ -141,12 +103,51 @@ const Visualization = React.createClass({
</div>
</div>
)
},
}
getQueryText(queryConfigs, index) {
// rawText can be null
return _.get(queryConfigs, [`${index}`, 'rawText'], '') || ''
},
})
}
}
Visualization.defaultProps = {
cellName: '',
cellType: '',
}
const {arrayOf, bool, func, number, shape, string} = PropTypes
Visualization.contextTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
Visualization.propTypes = {
cellName: string,
cellType: string,
autoRefresh: number.isRequired,
templates: arrayOf(shape()),
isInDataExplorer: bool,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
activeQueryIndex: number,
height: string,
heightPixels: number,
editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
}),
}),
resizerBottomHeight: number,
}
export default Visualization

View File

@ -32,7 +32,7 @@ const WriteDataBody = ({
<p>OR</p>
<input
type="file"
onChange={e => handleFile(e, false)}
onChange={handleFile(false)}
className="write-data-form--upload"
ref={fileInput}
accept="text/*, application/gzip"

View File

@ -21,26 +21,17 @@ class WriteDataForm extends Component {
dragClass: 'drag-none',
isUploading: false,
}
this.handleSelectDatabase = ::this.handleSelectDatabase
this.handleSubmit = ::this.handleSubmit
this.handleClickOutside = ::this.handleClickOutside
this.handleKeyUp = ::this.handleKeyUp
this.handleEdit = ::this.handleEdit
this.handleFile = ::this.handleFile
this.toggleWriteView = ::this.toggleWriteView
this.handleFileOpen = ::this.handleFileOpen
}
toggleWriteView(isManual) {
toggleWriteView = isManual => () => {
this.setState({isManual})
}
handleSelectDatabase(item) {
handleSelectDatabase = item => {
this.setState({selectedDatabase: item.text})
}
handleClickOutside(e) {
handleClickOutside = e => {
// guard against clicking to close error notification
if (e.target.className === OVERLAY_TECHNOLOGY) {
const {onClose} = this.props
@ -48,7 +39,7 @@ class WriteDataForm extends Component {
}
}
handleKeyUp(e) {
handleKeyUp = e => {
e.stopPropagation()
if (e.key === 'Escape') {
const {onClose} = this.props
@ -56,7 +47,7 @@ class WriteDataForm extends Component {
}
}
async handleSubmit() {
handleSubmit = async () => {
const {onClose, source, writeLineProtocol} = this.props
const {inputContent, uploadContent, selectedDatabase, isManual} = this.state
const content = isManual ? inputContent : uploadContent
@ -73,11 +64,11 @@ class WriteDataForm extends Component {
}
}
handleEdit(e) {
handleEdit = e => {
this.setState({inputContent: e.target.value.trim()})
}
handleFile(e, drop) {
handleFile = drop => e => {
let file
if (drop) {
file = e.dataTransfer.files[0]
@ -101,33 +92,18 @@ class WriteDataForm extends Component {
}
}
handleDragOver(e) {
handleDragOver = e => {
e.preventDefault()
e.stopPropagation()
}
handleDragClass(entering) {
return e => {
e.preventDefault()
if (entering) {
this.setState({
dragClass: 'drag-over',
})
} else {
this.setState({
dragClass: 'drag-none',
})
}
}
}
handleDragEnter(e) {
handleDragEnter = e => {
dragCounter += 1
e.preventDefault()
this.setState({dragClass: 'drag-over'})
}
handleDragLeave(e) {
handleDragLeave = e => {
dragCounter -= 1
e.preventDefault()
if (dragCounter === 0) {
@ -135,21 +111,23 @@ class WriteDataForm extends Component {
}
}
handleFileOpen() {
handleFileOpen = () => {
this.fileInput.click()
}
handleFileInputRef = el => (this.fileInput = el)
render() {
const {onClose, errorThrown} = this.props
const {dragClass} = this.state
return (
<div
onDrop={e => this.handleFile(e, true)}
onDrop={this.handleFile(true)}
onDragOver={this.handleDragOver}
onDragEnter={e => this.handleDragEnter(e)}
onDragExit={e => this.handleDragLeave(e)}
onDragLeave={e => this.handleDragLeave(e)}
onDragEnter={this.handleDragEnter}
onDragExit={this.handleDragLeave}
onDragLeave={this.handleDragLeave}
className={classnames(OVERLAY_TECHNOLOGY, dragClass)}
>
<div className="write-data-form">
@ -162,7 +140,7 @@ class WriteDataForm extends Component {
/>
<WriteDataBody
{...this.state}
fileInput={el => (this.fileInput = el)}
fileInput={this.handleFileInputRef}
handleEdit={this.handleEdit}
handleFile={this.handleFile}
handleKeyUp={this.handleKeyUp}

View File

@ -19,13 +19,13 @@ const WriteDataHeader = ({
/>
<ul className="nav nav-tablist nav-tablist-sm">
<li
onClick={() => toggleWriteView(false)}
onClick={toggleWriteView(false)}
className={isManual ? '' : 'active'}
>
File Upload
</li>
<li
onClick={() => toggleWriteView(true)}
onClick={toggleWriteView(true)}
className={isManual ? 'active' : ''}
data-test="manual-entry-button"
>

View File

@ -28,27 +28,23 @@ class DataExplorer extends Component {
}
}
getChildContext() {
return {source: this.props.source}
}
handleSetActiveQueryIndex(index) {
handleSetActiveQueryIndex = index => {
this.setState({activeQueryIndex: index})
}
handleDeleteQuery(index) {
handleDeleteQuery = index => {
const {queryConfigs, queryConfigActions} = this.props
const query = queryConfigs[index]
queryConfigActions.deleteQuery(query.id)
}
handleAddQuery() {
handleAddQuery = () => {
const newIndex = this.props.queryConfigs.length
this.props.queryConfigActions.addQuery()
this.handleSetActiveQueryIndex(newIndex)
}
getActiveQuery() {
getActiveQuery = () => {
const {activeQueryIndex} = this.state
const {queryConfigs} = this.props
const activeQuery = queryConfigs[activeQueryIndex]
@ -57,6 +53,14 @@ class DataExplorer extends Component {
return activeQuery || defaultQuery
}
handleCloseWriteData = () => {
this.setState({showWriteForm: false})
}
handleOpenWriteData = () => {
this.setState({showWriteForm: true})
}
render() {
const {
autoRefresh,
@ -82,19 +86,19 @@ class DataExplorer extends Component {
{showWriteForm
? <OverlayTechnologies>
<WriteDataForm
selectedDatabase={selectedDatabase}
errorThrown={errorThrownAction}
onClose={() => this.setState({showWriteForm: false})}
source={source}
errorThrown={errorThrownAction}
selectedDatabase={selectedDatabase}
onClose={this.handleCloseWriteData}
writeLineProtocol={writeLineProtocol}
/>
</OverlayTechnologies>
: null}
<Header
actions={{handleChooseAutoRefresh, setTimeRange}}
autoRefresh={autoRefresh}
timeRange={timeRange}
showWriteForm={() => this.setState({showWriteForm: true})}
autoRefresh={autoRefresh}
actions={{handleChooseAutoRefresh, setTimeRange}}
showWriteForm={this.handleOpenWriteData}
/>
<ResizeContainer
containerClass="page-contents"
@ -109,11 +113,11 @@ class DataExplorer extends Component {
actions={queryConfigActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
setActiveQueryIndex={::this.handleSetActiveQueryIndex}
onDeleteQuery={::this.handleDeleteQuery}
onAddQuery={::this.handleAddQuery}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
onAddQuery={this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={::this.getActiveQuery()}
activeQuery={this.getActiveQuery()}
/>
<Visualization
isInDataExplorer={true}

View File

@ -0,0 +1,82 @@
import React, {PropTypes, Component} from 'react'
import shallowCompare from 'react-addons-shallow-compare'
import {Link} from 'react-router'
import classnames from 'classnames'
import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing'
class HostRow extends Component {
constructor(props) {
super(props)
}
shouldComponentUpdate(nextProps) {
return shallowCompare(this, nextProps)
}
render() {
const {host, source} = this.props
const {name, cpu, load, apps = []} = host
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
return (
<tr>
<td style={{width: colName}}>
<Link to={`/sources/${source.id}/hosts/${name}`}>
{name}
</Link>
</td>
<td style={{width: colStatus}}>
<div
className={classnames(
'table-dot',
Math.max(host.deltaUptime || 0, host.winDeltaUptime || 0) > 0
? 'dot-success'
: 'dot-critical'
)}
/>
</td>
<td style={{width: colCPU}} className="monotype">
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}
</td>
<td style={{width: colLoad}} className="monotype">
{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}
</td>
<td>
{apps.map((app, index) => {
return (
<span key={app}>
<Link
style={{marginLeft: '2px'}}
to={{
pathname: `/sources/${source.id}/hosts/${name}`,
query: {app},
}}
>
{app}
</Link>
{index === apps.length - 1 ? null : ', '}
</span>
)
})}
</td>
</tr>
)
}
}
HostRow.propTypes = {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
host: PropTypes.shape({
name: PropTypes.string,
cpu: PropTypes.number,
load: PropTypes.number,
deltaUptime: PropTypes.number.required,
apps: PropTypes.arrayOf(PropTypes.string.isRequired),
}),
}
export default HostRow

View File

@ -1,38 +1,21 @@
import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import shallowCompare from 'react-addons-shallow-compare'
import classnames from 'classnames'
import SearchBar from 'src/hosts/components/SearchBar'
import HostRow from 'src/hosts/components/HostRow'
import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing'
const {arrayOf, bool, number, shape, string} = PropTypes
class HostsTable extends Component {
constructor(props) {
super(props)
const HostsTable = React.createClass({
propTypes: {
hosts: arrayOf(
shape({
name: string,
cpu: number,
load: number,
apps: arrayOf(string.isRequired),
})
),
hostsLoading: bool,
hostsError: string,
source: shape({
id: string.isRequired,
name: string.isRequired,
}).isRequired,
},
getInitialState() {
return {
this.state = {
searchTerm: '',
sortDirection: null,
sortKey: null,
}
},
}
filter(allHosts, searchTerm) {
const filterText = searchTerm.toLowerCase()
@ -53,7 +36,7 @@ const HostsTable = React.createClass({
tagResult
)
})
},
}
sort(hosts, key, direction) {
switch (direction) {
@ -64,13 +47,13 @@ const HostsTable = React.createClass({
default:
return hosts
}
},
}
updateSearchTerm(term) {
updateSearchTerm = term => {
this.setState({searchTerm: term})
},
}
updateSort(key) {
updateSort = key => () => {
// if we're using the key, reverse order; otherwise, set it with ascending
if (this.state.sortKey === key) {
const reverseDirection =
@ -79,9 +62,9 @@ const HostsTable = React.createClass({
} else {
this.setState({sortKey: key, sortDirection: 'asc'})
}
},
}
sortableClasses(key) {
sortableClasses = key => {
if (this.state.sortKey === key) {
if (this.state.sortDirection === 'asc') {
return 'sortable-header sorting-ascending'
@ -89,7 +72,7 @@ const HostsTable = React.createClass({
return 'sortable-header sorting-descending'
}
return 'sortable-header'
},
}
render() {
const {searchTerm, sortKey, sortDirection} = this.state
@ -128,28 +111,28 @@ const HostsTable = React.createClass({
<thead>
<tr>
<th
onClick={() => this.updateSort('name')}
onClick={this.updateSort('name')}
className={this.sortableClasses('name')}
style={{width: colName}}
>
Host
</th>
<th
onClick={() => this.updateSort('deltaUptime')}
onClick={this.updateSort('deltaUptime')}
className={this.sortableClasses('deltaUptime')}
style={{width: colStatus}}
>
Status
</th>
<th
onClick={() => this.updateSort('cpu')}
onClick={this.updateSort('cpu')}
className={this.sortableClasses('cpu')}
style={{width: colCPU}}
>
CPU
</th>
<th
onClick={() => this.updateSort('load')}
onClick={this.updateSort('load')}
className={this.sortableClasses('load')}
style={{width: colLoad}}
>
@ -160,9 +143,9 @@ const HostsTable = React.createClass({
</thead>
<tbody>
{sortedHosts.map(h => {
return <HostRow key={h.name} host={h} source={source} />
})}
{sortedHosts.map(h =>
<HostRow key={h.name} host={h} source={source} />
)}
</tbody>
</table>
: <div className="generic-empty-state">
@ -171,119 +154,26 @@ const HostsTable = React.createClass({
</div>
</div>
)
},
})
}
}
const HostRow = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
host: PropTypes.shape({
name: PropTypes.string,
cpu: PropTypes.number,
load: PropTypes.number,
deltaUptime: PropTypes.number.required,
apps: PropTypes.arrayOf(PropTypes.string.isRequired),
}),
},
const {arrayOf, bool, number, shape, string} = PropTypes
shouldComponentUpdate(nextProps) {
return shallowCompare(this, nextProps)
},
render() {
const {host, source} = this.props
const {name, cpu, load, apps = []} = host
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
return (
<tr>
<td style={{width: colName}}>
<Link to={`/sources/${source.id}/hosts/${name}`}>
{name}
</Link>
</td>
<td style={{width: colStatus}}>
<div
className={classnames(
'table-dot',
Math.max(host.deltaUptime || 0, host.winDeltaUptime || 0) > 0
? 'dot-success'
: 'dot-critical'
)}
/>
</td>
<td style={{width: colCPU}} className="monotype">
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}
</td>
<td style={{width: colLoad}} className="monotype">
{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}
</td>
<td>
{apps.map((app, index) => {
return (
<span key={app}>
<Link
style={{marginLeft: '2px'}}
to={{
pathname: `/sources/${source.id}/hosts/${name}`,
query: {app},
}}
>
{app}
</Link>
{index === apps.length - 1 ? null : ', '}
</span>
)
})}
</td>
</tr>
)
},
})
const SearchBar = React.createClass({
propTypes: {
onSearch: PropTypes.func.isRequired,
},
getInitialState() {
return {
searchTerm: '',
}
},
componentWillMount() {
const waitPeriod = 300
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
},
handleSearch() {
this.props.onSearch(this.state.searchTerm)
},
handleChange() {
this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch)
},
render() {
return (
<div className="users__search-widget input-group">
<input
type="text"
className="form-control"
placeholder="Filter by Host..."
ref="searchInput"
onChange={this.handleChange}
/>
<div className="input-group-addon">
<span className="icon search" aria-hidden="true" />
</div>
</div>
)
},
})
HostsTable.propTypes = {
hosts: arrayOf(
shape({
name: string,
cpu: number,
load: number,
apps: arrayOf(string.isRequired),
})
),
hostsLoading: bool,
hostsError: string,
source: shape({
id: string.isRequired,
name: string.isRequired,
}).isRequired,
}
export default HostsTable

View File

@ -0,0 +1,49 @@
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
class SearchBar extends Component {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
componentWillMount() {
const waitPeriod = 300
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
}
handleSearch = () => {
this.props.onSearch(this.state.searchTerm)
}
handleChange = () => {
this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch)
}
render() {
return (
<div className="users__search-widget input-group">
<input
type="text"
className="form-control"
placeholder="Filter by Host..."
ref="searchInput"
onChange={this.handleChange}
/>
<div className="input-group-addon">
<span className="icon search" aria-hidden="true" />
</div>
</div>
)
}
}
const {func} = PropTypes
SearchBar.propTypes = {
onSearch: func.isRequired,
}
export default SearchBar

View File

@ -2,11 +2,7 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {
getKapacitorConfig,
updateKapacitorConfigSection,
testAlertOutput,
} from 'shared/apis'
import {getKapacitorConfig, updateKapacitorConfigSection} from 'shared/apis'
import {
AlertaConfig,
@ -25,15 +21,11 @@ import {
class AlertTabs extends Component {
constructor(props) {
super(props)
this.state = {
selectedEndpoint: 'smtp',
configSections: null,
}
this.refreshKapacitorConfig = ::this.refreshKapacitorConfig
this.getSection = ::this.getSection
this.handleSaveConfig = ::this.handleSaveConfig
this.handleTest = ::this.handleTest
this.sanitizeProperties = ::this.sanitizeProperties
}
componentDidMount() {
@ -46,7 +38,7 @@ class AlertTabs extends Component {
}
}
refreshKapacitorConfig(kapacitor) {
refreshKapacitorConfig = kapacitor => {
getKapacitorConfig(kapacitor)
.then(({data: {sections}}) => {
this.setState({configSections: sections})
@ -60,11 +52,15 @@ class AlertTabs extends Component {
})
}
getSection(sections, section) {
getSection = (sections, section) => {
return _.get(sections, [section, 'elements', '0'], null)
}
handleSaveConfig(section, properties) {
handleGetSection = (sections, section) => () => {
return this.getSection(sections, section)
}
handleSaveConfig = section => properties => {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend)
@ -84,25 +80,8 @@ class AlertTabs extends Component {
}
}
handleTest(section, properties) {
const propsToSend = this.sanitizeProperties(section, properties)
testAlertOutput(this.props.kapacitor, section, propsToSend)
.then(() => {
this.props.addFlashMessage({
type: 'success',
text: 'Slack test message sent',
})
})
.catch(() => {
this.props.addFlashMessage({
type: 'error',
text: 'There was an error testing the slack alert',
})
})
}
sanitizeProperties(section, properties) {
const cleanProps = Object.assign({}, properties, {enabled: true})
sanitizeProperties = (section, properties) => {
const cleanProps = {...properties, enabled: true}
const {redacted} = this.getSection(this.state.configSections, section)
if (redacted && redacted.length) {
redacted.forEach(badProp => {
@ -111,6 +90,7 @@ class AlertTabs extends Component {
}
})
}
return cleanProps
}
@ -121,16 +101,12 @@ class AlertTabs extends Component {
return null
}
const test = properties => {
this.handleTest('slack', properties)
}
const supportedConfigs = {
alerta: {
type: 'Alerta',
renderComponent: () =>
<AlertaConfig
onSave={p => this.handleSaveConfig('alerta', p)}
onSave={this.handleSaveConfig('alerta')}
config={this.getSection(configSections, 'alerta')}
/>,
},
@ -138,7 +114,7 @@ class AlertTabs extends Component {
type: 'HipChat',
renderComponent: () =>
<HipChatConfig
onSave={p => this.handleSaveConfig('hipchat', p)}
onSave={this.handleSaveConfig('hipchat')}
config={this.getSection(configSections, 'hipchat')}
/>,
},
@ -146,7 +122,7 @@ class AlertTabs extends Component {
type: 'OpsGenie',
renderComponent: () =>
<OpsGenieConfig
onSave={p => this.handleSaveConfig('opsgenie', p)}
onSave={this.handleSaveConfig('opsgenie')}
config={this.getSection(configSections, 'opsgenie')}
/>,
},
@ -154,7 +130,7 @@ class AlertTabs extends Component {
type: 'PagerDuty',
renderComponent: () =>
<PagerDutyConfig
onSave={p => this.handleSaveConfig('pagerduty', p)}
onSave={this.handleSaveConfig('pagerduty')}
config={this.getSection(configSections, 'pagerduty')}
/>,
},
@ -162,7 +138,7 @@ class AlertTabs extends Component {
type: 'Pushover',
renderComponent: () =>
<PushoverConfig
onSave={p => this.handleSaveConfig('pushover', p)}
onSave={this.handleSaveConfig('pushover')}
config={this.getSection(configSections, 'pushover')}
/>,
},
@ -170,7 +146,7 @@ class AlertTabs extends Component {
type: 'Sensu',
renderComponent: () =>
<SensuConfig
onSave={p => this.handleSaveConfig('sensu', p)}
onSave={this.handleSaveConfig('sensu')}
config={this.getSection(configSections, 'sensu')}
/>,
},
@ -178,8 +154,7 @@ class AlertTabs extends Component {
type: 'Slack',
renderComponent: () =>
<SlackConfig
onSave={p => this.handleSaveConfig('slack', p)}
onTest={test}
onSave={this.handleSaveConfig('slack')}
config={this.getSection(configSections, 'slack')}
/>,
},
@ -187,7 +162,7 @@ class AlertTabs extends Component {
type: 'SMTP',
renderComponent: () =>
<SMTPConfig
onSave={p => this.handleSaveConfig('smtp', p)}
onSave={this.handleSaveConfig('smtp')}
config={this.getSection(configSections, 'smtp')}
/>,
},
@ -195,7 +170,7 @@ class AlertTabs extends Component {
type: 'Talk',
renderComponent: () =>
<TalkConfig
onSave={p => this.handleSaveConfig('talk', p)}
onSave={this.handleSaveConfig('talk')}
config={this.getSection(configSections, 'talk')}
/>,
},
@ -203,7 +178,7 @@ class AlertTabs extends Component {
type: 'Telegram',
renderComponent: () =>
<TelegramConfig
onSave={p => this.handleSaveConfig('telegram', p)}
onSave={this.handleSaveConfig('telegram')}
config={this.getSection(configSections, 'telegram')}
/>,
},
@ -211,7 +186,7 @@ class AlertTabs extends Component {
type: 'VictorOps',
renderComponent: () =>
<VictorOpsConfig
onSave={p => this.handleSaveConfig('victorops', p)}
onSave={this.handleSaveConfig('victorops')}
config={this.getSection(configSections, 'victorops')}
/>,
},

View File

@ -76,16 +76,16 @@ const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
className="form-control-static"
type="checkbox"
defaultChecked={rule.status === 'enabled'}
onClick={() => onChangeRuleStatus(rule)}
onClick={onChangeRuleStatus(rule)}
/>
<label htmlFor={`kapacitor-enabled ${rule.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right table-cell-nowrap">
<button className="btn btn-info btn-xs" onClick={() => onRead(rule)}>
<button className="btn btn-info btn-xs" onClick={onRead(rule)}>
View TICKscript
</button>
<button className="btn btn-danger btn-xs" onClick={() => onDelete(rule)}>
<button className="btn btn-danger btn-xs" onClick={onDelete(rule)}>
Delete
</button>
</td>

View File

@ -1,142 +1,85 @@
import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
import SourceIndicator from 'shared/components/SourceIndicator'
import React, {PropTypes, Component} from 'react'
import RuleHeaderEdit from 'src/kapacitor/components/RuleHeaderEdit'
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
export const RuleHeader = React.createClass({
propTypes: {
source: PropTypes.shape({}).isRequired,
onSave: PropTypes.func.isRequired,
rule: PropTypes.shape({}).isRequired,
actions: PropTypes.shape({
updateRuleName: PropTypes.func.isRequired,
}).isRequired,
validationError: PropTypes.string.isRequired,
onChooseTimeRange: PropTypes.func.isRequired,
timeRange: PropTypes.shape({}).isRequired,
},
class RuleHeader extends Component {
constructor(props) {
super(props)
getInitialState() {
return {
this.state = {
isEditingName: false,
}
},
}
handleEditName(e, rule) {
toggleEditName = () => {
this.setState({isEditingName: !this.state.isEditingName})
}
handleEditName = rule => e => {
if (e.key === 'Enter') {
const {updateRuleName} = this.props.actions
const name = this.ruleName.value
updateRuleName(rule.id, name)
updateRuleName(rule.id, e.target.value)
this.toggleEditName()
}
if (e.key === 'Escape') {
this.toggleEditName()
}
},
}
handleEditNameBlur(rule) {
handleEditNameBlur = rule => e => {
const {updateRuleName} = this.props.actions
const name = this.ruleName.value
updateRuleName(rule.id, name)
updateRuleName(rule.id, e.target.value)
this.toggleEditName()
},
toggleEditName() {
this.setState({isEditingName: !this.state.isEditingName})
},
}
render() {
const {
rule,
source,
onSave,
timeRange,
validationError,
onChooseTimeRange,
} = this.props
const {isEditingName} = this.state
return (
<div className="page-header">
<div className="page-header__container">
{this.renderEditName()}
{this.renderSave()}
<RuleHeaderEdit
rule={rule}
isEditing={isEditingName}
onToggleEdit={this.toggleEditName}
onEditName={this.handleEditName}
onEditNameBlur={this.handleEditNameBlur}
/>
<RuleHeaderSave
source={source}
onSave={onSave}
timeRange={timeRange}
validationError={validationError}
onChooseTimeRange={onChooseTimeRange}
/>
</div>
</div>
)
},
}
}
renderSave() {
const {
validationError,
onSave,
timeRange,
onChooseTimeRange,
source,
} = this.props
const saveButton = validationError
? <button
className="btn btn-success btn-sm disabled"
data-for="save-kapacitor-tooltip"
data-tip={validationError}
>
Save Rule
</button>
: <button className="btn btn-success btn-sm" onClick={onSave}>
Save Rule
</button>
const {func, shape, string} = PropTypes
return (
<div className="page-header__right">
<SourceIndicator sourceName={source.name} />
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
{saveButton}
<ReactTooltip
id="save-kapacitor-tooltip"
effect="solid"
html={true}
offset={{bottom: 4}}
place="bottom"
class="influx-tooltip kapacitor-tooltip place-bottom"
/>
</div>
)
},
renderEditName() {
const {rule} = this.props
const {isEditingName} = this.state
const name = isEditingName
? <input
className="page-header--editing kapacitor-theme"
autoFocus={true}
defaultValue={rule.name}
ref={r => (this.ruleName = r)}
onKeyDown={e => this.handleEditName(e, rule)}
onBlur={() => this.handleEditNameBlur(rule)}
placeholder="Name your rule"
spellCheck={false}
autoComplete={false}
/>
: <div className="page-header__left">
<h1
className="page-header__title page-header--editable kapacitor-theme"
onClick={this.toggleEditName}
data-for="rename-kapacitor-tooltip"
data-tip="Click to Rename"
>
{rule.name}
<span className="icon pencil" />
<ReactTooltip
id="rename-kapacitor-tooltip"
delayShow={200}
effect="solid"
html={true}
offset={{top: 2}}
place="bottom"
class="influx-tooltip kapacitor-tooltip place-bottom"
/>
</h1>
</div>
return name
},
})
RuleHeader.propTypes = {
source: shape({}).isRequired,
onSave: func.isRequired,
rule: shape({}).isRequired,
actions: shape({
updateRuleName: func.isRequired,
}).isRequired,
validationError: string.isRequired,
onChooseTimeRange: func.isRequired,
timeRange: shape({}).isRequired,
}
export default RuleHeader

View File

@ -0,0 +1,53 @@
import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip'
const RuleHeaderEdit = ({
rule,
isEditing,
onToggleEdit,
onEditName,
onEditNameBlur,
}) =>
isEditing
? <input
className="page-header--editing kapacitor-theme"
autoFocus={true}
defaultValue={rule.name}
onKeyDown={onEditName(rule)}
onBlur={onEditNameBlur(rule)}
placeholder="Name your rule"
spellCheck={false}
autoComplete={false}
/>
: <div className="page-header__left">
<h1
className="page-header__title page-header--editable kapacitor-theme"
onClick={onToggleEdit}
data-for="rename-kapacitor-tooltip"
data-tip="Click to Rename"
>
{rule.name}
<span className="icon pencil" />
<ReactTooltip
id="rename-kapacitor-tooltip"
delayShow={200}
effect="solid"
html={true}
offset={{top: 2}}
place="bottom"
class="influx-tooltip kapacitor-tooltip place-bottom"
/>
</h1>
</div>
const {bool, func, shape} = PropTypes
RuleHeaderEdit.propTypes = {
rule: shape(),
isEditing: bool.isRequired,
onToggleEdit: func.isRequired,
onEditName: func.isRequired,
onEditNameBlur: func.isRequired,
}
export default RuleHeaderEdit

View File

@ -0,0 +1,51 @@
import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
import SourceIndicator from 'shared/components/SourceIndicator'
const RuleHeaderSave = ({
source,
onSave,
timeRange,
validationError,
onChooseTimeRange,
}) =>
<div className="page-header__right">
<SourceIndicator sourceName={source.name} />
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
{validationError
? <button
className="btn btn-success btn-sm disabled"
data-for="save-kapacitor-tooltip"
data-tip={validationError}
>
Save Rule
</button>
: <button className="btn btn-success btn-sm" onClick={onSave}>
Save Rule
</button>}
<ReactTooltip
id="save-kapacitor-tooltip"
effect="solid"
html={true}
offset={{bottom: 4}}
place="bottom"
class="influx-tooltip kapacitor-tooltip place-bottom"
/>
</div>
const {func, shape, string} = PropTypes
RuleHeaderSave.propTypes = {
source: shape({}).isRequired,
onSave: func.isRequired,
validationError: string.isRequired,
onChooseTimeRange: func.isRequired,
timeRange: shape({}).isRequired,
}
export default RuleHeaderSave

View File

@ -15,17 +15,14 @@ class RuleMessage extends Component {
this.state = {
selectedAlertNodeName: null,
}
this.handleChangeMessage = ::this.handleChangeMessage
this.handleChooseAlert = ::this.handleChooseAlert
}
handleChangeMessage() {
handleChangeMessage = e => {
const {actions, rule} = this.props
actions.updateMessage(rule.id, this.message.value)
actions.updateMessage(rule.id, e.target.value)
}
handleChooseAlert(item) {
handleChooseAlert = item => () => {
const {actions} = this.props
actions.updateAlerts(item.ruleID, [item.text])
actions.updateAlertNodes(item.ruleID, item.text, '')
@ -63,7 +60,7 @@ class RuleMessage extends Component {
className={classnames({
active: alert.text === selectedAlertNodeName,
})}
onClick={() => this.handleChooseAlert(alert)}
onClick={this.handleChooseAlert(alert)}
>
{alert.text}
</li>
@ -77,7 +74,10 @@ class RuleMessage extends Component {
updateDetails={actions.updateDetails}
updateAlertProperty={actions.updateAlertProperty}
/>
<RuleMessageText rule={rule} updateMessage={actions.updateMessage} />
<RuleMessageText
rule={rule}
updateMessage={this.handleChangeMessage}
/>
<RuleMessageTemplates
rule={rule}
updateMessage={actions.updateMessage}

View File

@ -8,11 +8,12 @@ import {
class RuleMessageOptions extends Component {
constructor(props) {
super(props)
this.getAlertPropertyValue = ::this.getAlertPropertyValue
}
getAlertPropertyValue(properties, name) {
getAlertPropertyValue = name => {
const {rule} = this.props
const {properties} = rule.alertNodes[0]
if (properties) {
const alertNodeProperty = properties.find(
property => property.name === name
@ -24,14 +25,26 @@ class RuleMessageOptions extends Component {
return ''
}
handleUpdateDetails = e => {
const {updateDetails, rule} = this.props
updateDetails(rule.id, e.target.value)
}
handleUpdateAlertNodes = e => {
const {updateAlertNodes, alertNodeName, rule} = this.props
updateAlertNodes(rule.id, alertNodeName, e.target.value)
}
handleUpdateAlertProperty = propertyName => e => {
const {updateAlertProperty, alertNodeName, rule} = this.props
updateAlertProperty(rule.id, alertNodeName, {
name: propertyName,
args: [e.target.value],
})
}
render() {
const {
rule,
alertNodeName,
updateAlertNodes,
updateDetails,
updateAlertProperty,
} = this.props
const {rule, alertNodeName} = this.props
const {args, details, properties} = RULE_ALERT_OPTIONS[alertNodeName]
return (
@ -47,8 +60,7 @@ class RuleMessageOptions extends Component {
style={{flex: '1 0 0%'}}
type="text"
placeholder={args.placeholder}
onChange={e =>
updateAlertNodes(rule.id, alertNodeName, e.target.value)}
onChange={this.handleUpdateAlertNodes}
value={ALERT_NODES_ACCESSORS[alertNodeName](rule)}
autoComplete="off"
spellCheck="false"
@ -87,15 +99,8 @@ class RuleMessageOptions extends Component {
}}
type="text"
placeholder={placeholder}
onChange={e =>
updateAlertProperty(rule.id, alertNodeName, {
name: propertyName,
args: [e.target.value],
})}
value={this.getAlertPropertyValue(
rule.alertNodes[0].properties,
propertyName
)}
onChange={this.handleUpdateAlertProperty(propertyName)}
value={this.getAlertPropertyValue(propertyName)}
autoComplete="off"
spellCheck="false"
/>
@ -110,8 +115,7 @@ class RuleMessageOptions extends Component {
<textarea
className="form-control form-malachite monotype rule-builder--message"
placeholder={details.placeholder ? details.placeholder : ''}
ref={r => (this.details = r)}
onChange={() => updateDetails(rule.id, this.details.value)}
onChange={this.handleUpdateDetails}
value={rule.details}
spellCheck={false}
/>

View File

@ -12,9 +12,12 @@ class RuleMessageTemplates extends Component {
super(props)
}
render() {
const {rule, updateMessage} = this.props
handleClickTemplate = template => () => {
const {updateMessage, rule} = this.props
updateMessage(rule.id, `${rule.message} ${template.label}`)
}
render() {
return (
<div className="rule-section--row rule-section--row-last rule-section--border-top">
<p>Templates:</p>
@ -23,8 +26,7 @@ class RuleMessageTemplates extends Component {
<CodeData
key={key}
template={template}
onClickTemplate={() =>
updateMessage(rule.id, `${rule.message} ${template.label}`)}
onClickTemplate={this.handleClickTemplate(template)}
/>
)
})}

View File

@ -1,25 +1,13 @@
import React, {Component, PropTypes} from 'react'
import React, {PropTypes} from 'react'
class RuleMessageText extends Component {
constructor(props) {
super(props)
}
render() {
const {rule, updateMessage} = this.props
return (
<textarea
className="form-control form-malachite monotype rule-builder--message"
ref={r => (this.message = r)}
onChange={() => updateMessage(rule.id, this.message.value)}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
)
}
}
const RuleMessageText = ({rule, updateMessage}) =>
<textarea
className="form-control form-malachite monotype rule-builder--message"
onChange={updateMessage}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
const {func, shape} = PropTypes

View File

@ -1,23 +1,13 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
const {bool, func, shape, string} = PropTypes
class AlertaConfig extends Component {
constructor(props) {
super(props)
}
const AlertaConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
environment: string,
origin: string,
token: bool,
url: string,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -28,7 +18,9 @@ const AlertaConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleTokenRef = r => (this.token = r)
render() {
const {environment, origin, token, url} = this.props.config.options
@ -62,7 +54,7 @@ const AlertaConfig = React.createClass({
<RedactedInput
defaultValue={token}
id="token"
refFunc={r => (this.token = r)}
refFunc={this.handleTokenRef}
/>
</div>
@ -84,7 +76,21 @@ const AlertaConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, shape, string} = PropTypes
AlertaConfig.propTypes = {
config: shape({
options: shape({
environment: string,
origin: string,
token: bool,
url: string,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default AlertaConfig

View File

@ -1,24 +1,15 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
import RedactedInput from './RedactedInput'
const {bool, func, shape, string} = PropTypes
class HipchatConfig extends Component {
constructor(props) {
super(props)
}
const HipchatConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -28,7 +19,9 @@ const HipchatConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleTokenRef = r => (this.token = r)
render() {
const {options} = this.props.config
@ -72,7 +65,7 @@ const HipchatConfig = React.createClass({
<RedactedInput
defaultValue={token}
id="token"
refFunc={r => (this.token = r)}
refFunc={this.handleTokenRef}
/>
</div>
@ -83,7 +76,20 @@ const HipchatConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, shape, string} = PropTypes
HipchatConfig.propTypes = {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default HipchatConfig

View File

@ -1,31 +1,21 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import RedactedInput from './RedactedInput'
const {array, arrayOf, bool, func, shape, string} = PropTypes
class OpsGenieConfig extends Component {
constructor(props) {
super(props)
const OpsGenieConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'api-key': bool,
teams: array,
recipients: array,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
const {teams, recipients} = props.config.options
getInitialState() {
const {teams, recipients} = this.props.config.options
return {
this.state = {
currentTeams: teams || [],
currentRecipients: recipients || [],
}
},
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -35,31 +25,33 @@ const OpsGenieConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleAddTeam(team) {
handleAddTeam = team => {
this.setState({currentTeams: this.state.currentTeams.concat(team)})
},
}
handleAddRecipient(recipient) {
handleAddRecipient = recipient => {
this.setState({
currentRecipients: this.state.currentRecipients.concat(recipient),
})
},
}
handleDeleteTeam(team) {
handleDeleteTeam = team => () => {
this.setState({
currentTeams: this.state.currentTeams.filter(t => t !== team),
})
},
}
handleDeleteRecipient(recipient) {
handleDeleteRecipient = recipient => () => {
this.setState({
currentRecipients: this.state.currentRecipients.filter(
r => r !== recipient
),
})
},
}
handleApiKeyRef = r => (this.apiKey = r)
render() {
const {options} = this.props.config
@ -73,7 +65,7 @@ const OpsGenieConfig = React.createClass({
<RedactedInput
defaultValue={apiKey}
id="api-key"
refFunc={r => (this.apiKey = r)}
refFunc={this.handleApiKeyRef}
/>
<label className="form-helper">
Note: a value of <code>true</code> indicates the OpsGenie API key
@ -101,18 +93,28 @@ const OpsGenieConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const TagInput = React.createClass({
propTypes: {
onAddTag: func.isRequired,
onDeleteTag: func.isRequired,
tags: arrayOf(string).isRequired,
title: string.isRequired,
},
const {array, arrayOf, bool, func, shape, string} = PropTypes
handleAddTag(e) {
OpsGenieConfig.propTypes = {
config: shape({
options: shape({
'api-key': bool,
teams: array,
recipients: array,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
class TagInput extends Component {
constructor(props) {
super(props)
}
handleAddTag = e => {
if (e.key === 'Enter') {
e.preventDefault()
const newItem = e.target.value.trim()
@ -124,11 +126,11 @@ const TagInput = React.createClass({
this.input.value = ''
onAddTag(newItem)
}
},
}
shouldAddToList(item, tags) {
return !_.isEmpty(item) && !tags.find(l => l === item)
},
}
render() {
const {title, tags, onDeleteTag} = this.props
@ -150,45 +152,39 @@ const TagInput = React.createClass({
<Tags tags={tags} onDeleteTag={onDeleteTag} />
</div>
)
},
})
}
}
const Tags = React.createClass({
propTypes: {
tags: arrayOf(string),
onDeleteTag: func,
},
TagInput.propTypes = {
onAddTag: func.isRequired,
onDeleteTag: func.isRequired,
tags: arrayOf(string).isRequired,
title: string.isRequired,
}
render() {
const {tags, onDeleteTag} = this.props
return (
<div className="input-tag-list">
{tags.map(item => {
return <Tag key={item} item={item} onDelete={onDeleteTag} />
})}
</div>
)
},
})
const Tags = ({tags, onDeleteTag}) =>
<div className="input-tag-list">
{tags.map(item => {
return <Tag key={item} item={item} onDelete={onDeleteTag} />
})}
</div>
const Tag = React.createClass({
propTypes: {
item: string,
onDelete: func,
},
Tags.propTypes = {
tags: arrayOf(string),
onDeleteTag: func,
}
render() {
const {item, onDelete} = this.props
const Tag = ({item, onDelete}) =>
<span key={item} className="input-tag-item">
<span>
{item}
</span>
<span className="icon remove" onClick={onDelete(item)} />
</span>
return (
<span key={item} className="input-tag-item">
<span>
{item}
</span>
<span className="icon remove" onClick={() => onDelete(item)} />
</span>
)
},
})
Tag.propTypes = {
item: string,
onDelete: func,
}
export default OpsGenieConfig

View File

@ -1,26 +1,20 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
const PagerDutyConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'service-key': PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
class PagerDutyConfig extends Component {
constructor(props) {
super(props)
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
serviceKey: this.serviceKey.value,
'service-key': this.serviceKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
}
render() {
const {options} = this.props.config
@ -62,7 +56,19 @@ const PagerDutyConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, shape, string} = PropTypes
PagerDutyConfig.propTypes = {
config: shape({
options: shape({
'service-key': bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default PagerDutyConfig

View File

@ -8,11 +8,9 @@ import {PUSHOVER_DOCS_LINK} from 'src/kapacitor/copy'
class PushoverConfig extends Component {
constructor(props) {
super(props)
this.handleSaveAlert = ::this.handleSaveAlert
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -24,6 +22,10 @@ class PushoverConfig extends Component {
this.props.onSave(properties)
}
handleUserKeyRef = r => (this.userKey = r)
handleTokenRef = r => (this.token = r)
render() {
const {options} = this.props.config
const {token, url} = options
@ -42,7 +44,7 @@ class PushoverConfig extends Component {
<RedactedInput
defaultValue={userKey}
id="user-key"
refFunc={r => (this.userKey = r)}
refFunc={this.handleUserKeyRef}
/>
</div>
@ -57,7 +59,7 @@ class PushoverConfig extends Component {
<RedactedInput
defaultValue={token}
id="token"
refFunc={r => (this.token = r)}
refFunc={this.handleTokenRef}
/>
</div>

View File

@ -8,6 +8,10 @@ class RedactedInput extends Component {
}
}
handleClick = () => {
this.setState({editing: true})
}
render() {
const {defaultValue, id, refFunc} = this.props
const {editing} = this.state
@ -18,12 +22,7 @@ class RedactedInput extends Component {
<span className="alert-value-set">
<span className="icon checkmark" /> Value set
</span>
<button
className="btn btn-xs btn-link"
onClick={() => {
this.setState({editing: true})
}}
>
<button className="btn btn-xs btn-link" onClick={this.handleClick}>
Change
</button>
<input

View File

@ -1,20 +1,11 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
const SMTPConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
host: PropTypes.string,
port: PropTypes.number,
username: PropTypes.string,
password: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
class SMTPConfig extends Component {
constructor(props) {
super(props)
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -26,7 +17,7 @@ const SMTPConfig = React.createClass({
}
this.props.onSave(properties)
},
}
render() {
const {host, port, from, username, password} = this.props.config.options
@ -96,7 +87,22 @@ const SMTPConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, number, shape, string} = PropTypes
SMTPConfig.propTypes = {
config: shape({
options: shape({
host: string,
port: number,
username: string,
password: bool,
from: string,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default SMTPConfig

View File

@ -1,17 +1,11 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
const SensuConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
source: PropTypes.string.isRequired,
addr: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
class SensuConfig extends Component {
constructor(props) {
super(props)
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -20,7 +14,7 @@ const SensuConfig = React.createClass({
}
this.props.onSave(properties)
},
}
render() {
const {source, addr} = this.props.config.options
@ -56,7 +50,19 @@ const SensuConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {func, shape, string} = PropTypes
SensuConfig.propTypes = {
config: shape({
options: shape({
source: string.isRequired,
addr: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default SensuConfig

View File

@ -1,32 +1,22 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
const SlackConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
url: PropTypes.bool.isRequired,
channel: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
onTest: PropTypes.func.isRequired,
},
getInitialState() {
return {
class SlackConfig extends Component {
constructor(props) {
super(props)
this.state = {
testEnabled: !!this.props.config.options.url,
}
},
}
componentWillReceiveProps(nextProps) {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
},
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -35,15 +25,9 @@ const SlackConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleTest(e) {
e.preventDefault()
this.props.onTest({
url: this.url.value,
channel: this.channel.value,
})
},
handleUrlRef = r => (this.url = r)
render() {
const {url, channel} = this.props.config.options
@ -61,7 +45,7 @@ const SlackConfig = React.createClass({
<RedactedInput
defaultValue={url}
id="url"
refFunc={r => (this.url = r)}
refFunc={this.handleUrlRef}
/>
</div>
@ -84,7 +68,19 @@ const SlackConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, shape, string} = PropTypes
SlackConfig.propTypes = {
config: shape({
options: shape({
url: bool.isRequired,
channel: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default SlackConfig

View File

@ -1,21 +1,13 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
const {bool, string, shape, func} = PropTypes
class TalkConfig extends Component {
constructor(props) {
super(props)
}
const TalkConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -24,7 +16,9 @@ const TalkConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleUrlRef = r => (this.url = r)
render() {
const {url, author_name: author} = this.props.config.options
@ -36,7 +30,7 @@ const TalkConfig = React.createClass({
<RedactedInput
defaultValue={url}
id="url"
refFunc={r => (this.url = r)}
refFunc={this.handleUrlRef}
/>
</div>
@ -58,7 +52,19 @@ const TalkConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, string, shape, func} = PropTypes
TalkConfig.propTypes = {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default TalkConfig

View File

@ -1,26 +1,15 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
import RedactedInput from './RedactedInput'
const {bool, func, shape, string} = PropTypes
class TelegramConfig extends Component {
constructor(props) {
super(props)
}
const TelegramConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
let parseMode
@ -40,7 +29,9 @@ const TelegramConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleTokenRef = r => (this.token = r)
render() {
const {options} = this.props.config
@ -76,7 +67,7 @@ const TelegramConfig = React.createClass({
<RedactedInput
defaultValue={token}
id="token"
refFunc={r => (this.token = r)}
refFunc={this.handleTokenRef}
/>
</div>
@ -166,7 +157,22 @@ const TelegramConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, func, shape, string} = PropTypes
TelegramConfig.propTypes = {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default TelegramConfig

View File

@ -1,20 +1,13 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
const VictorOpsConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'api-key': PropTypes.bool,
'routing-key': PropTypes.string,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
class VictorOpsConfig extends Component {
constructor(props) {
super(props)
}
handleSaveAlert(e) {
handleSaveAlert = e => {
e.preventDefault()
const properties = {
@ -24,7 +17,9 @@ const VictorOpsConfig = React.createClass({
}
this.props.onSave(properties)
},
}
handleApiRef = r => (this.apiKey = r)
render() {
const {options} = this.props.config
@ -39,7 +34,7 @@ const VictorOpsConfig = React.createClass({
<RedactedInput
defaultValue={apiKey}
id="api-key"
refFunc={r => (this.apiKey = r)}
refFunc={this.handleApiRef}
/>
</div>
@ -72,7 +67,20 @@ const VictorOpsConfig = React.createClass({
</div>
</form>
)
},
})
}
}
const {bool, shape, string, func} = PropTypes
VictorOpsConfig.propTypes = {
config: shape({
options: shape({
'api-key': bool,
'routing-key': string,
url: string,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default VictorOpsConfig

View File

@ -164,7 +164,7 @@ KapacitorPage.propTypes = {
source: shape({
id: string.isRequired,
url: string.isRequired,
kapacitors: array.isRequired,
kapacitors: array,
}),
}

View File

@ -13,11 +13,6 @@ class KapacitorRulesPage extends Component {
loading: true,
tickscript: null,
}
this.handleDeleteRule = ::this.handleDeleteRule
this.handleRuleStatus = ::this.handleRuleStatus
this.handleReadTickscript = ::this.handleReadTickscript
this.handleCloseTickscript = ::this.handleCloseTickscript
}
async componentDidMount() {
@ -30,12 +25,12 @@ class KapacitorRulesPage extends Component {
this.setState({loading: false, hasKapacitor: !!kapacitor})
}
handleDeleteRule(rule) {
handleDeleteRule = rule => () => {
const {actions} = this.props
actions.deleteRule(rule)
}
handleRuleStatus(rule) {
handleRuleStatus = rule => () => {
const {actions} = this.props
const status = rule.status === 'enabled' ? 'disabled' : 'enabled'
@ -43,11 +38,11 @@ class KapacitorRulesPage extends Component {
actions.updateRuleStatusSuccess(rule.id, status)
}
handleReadTickscript({tickscript}) {
handleReadTickscript = ({tickscript}) => () => {
this.setState({tickscript})
}
handleCloseTickscript() {
handleCloseTickscript = () => {
this.setState({tickscript: null})
}

View File

@ -1,53 +1,42 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import autoRefreshItems from 'hson!shared/data/autoRefreshes.hson'
const {number, func} = PropTypes
const AutoRefreshDropdown = React.createClass({
autobind: false,
propTypes: {
selected: number.isRequired,
onChoose: func.isRequired,
},
getInitialState() {
return {
class AutoRefreshDropdown extends Component {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
},
}
findAutoRefreshItem(milliseconds) {
return autoRefreshItems.find(values => values.milliseconds === milliseconds)
},
}
handleClickOutside() {
this.setState({isOpen: false})
},
}
handleSelection(milliseconds) {
handleSelection = milliseconds => () => {
this.props.onChoose(milliseconds)
this.setState({isOpen: false})
},
}
toggleMenu() {
this.setState({isOpen: !this.state.isOpen})
},
toggleMenu = () => this.setState({isOpen: !this.state.isOpen})
render() {
const self = this
const {selected} = self.props
const {isOpen} = self.state
const {selected} = this.props
const {isOpen} = this.state
const {milliseconds, inputValue} = this.findAutoRefreshItem(selected)
return (
<div className={classnames('dropdown dropdown-160', {open: isOpen})}>
<div
className="btn btn-sm btn-default dropdown-toggle"
onClick={() => self.toggleMenu()}
onClick={this.toggleMenu}
>
<span
className={classnames(
@ -62,22 +51,24 @@ const AutoRefreshDropdown = React.createClass({
</div>
<ul className="dropdown-menu">
<li className="dropdown-header">AutoRefresh Interval</li>
{autoRefreshItems.map(item => {
return (
<li className="dropdown-item" key={item.menuOption}>
<a
href="#"
onClick={() => self.handleSelection(item.milliseconds)}
>
{item.menuOption}
</a>
</li>
)
})}
{autoRefreshItems.map(item =>
<li className="dropdown-item" key={item.menuOption}>
<a href="#" onClick={this.handleSelection(item.milliseconds)}>
{item.menuOption}
</a>
</li>
)}
</ul>
</div>
)
},
})
}
}
const {number, func} = PropTypes
AutoRefreshDropdown.propTypes = {
selected: number.isRequired,
onChoose: func.isRequired,
}
export default OnClickOutside(AutoRefreshDropdown)

View File

@ -5,24 +5,22 @@ import onClickOutside from 'shared/components/OnClickOutside'
class ClickOutsideInput extends Component {
constructor(props) {
super(props)
this.handleClickOutside = ::this.handleClickOutside
}
handleClickOutside(e) {
this.props.handleClickOutsideCustomValueInput(e)
handleClickOutside = e => {
this.props.handleClickOutsideInput(e)
}
render() {
const {
id,
type,
customPlaceholder,
onGetRef,
customValue,
onFocus,
onChange,
onGetRef,
onKeyDown,
customValue,
customPlaceholder,
} = this.props
return (
@ -53,7 +51,7 @@ ClickOutsideInput.propTypes = {
onFocus: func.isRequired,
onChange: func.isRequired,
onKeyDown: func.isRequired,
handleClickOutsideCustomValueInput: func.isRequired,
handleClickOutsideInput: func.isRequired,
}
export default onClickOutside(ClickOutsideInput)

View File

@ -1,27 +1,46 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import classnames from 'classnames'
const ConfirmButtons = ({onConfirm, item, onCancel, buttonSize, isDisabled}) =>
<div className="confirm-buttons">
<button
className={classnames('btn btn-info btn-square', {
[buttonSize]: buttonSize,
})}
onClick={() => onCancel(item)}
>
<span className="icon remove" />
</button>
<button
className={classnames('btn btn-success btn-square', {
[buttonSize]: buttonSize,
})}
disabled={isDisabled}
title={isDisabled ? 'Cannot Save' : 'Save'}
onClick={() => onConfirm(item)}
>
<span className="icon checkmark" />
</button>
</div>
class ConfirmButtons extends Component {
constructor(props) {
super(props)
}
handleConfirm = item => () => {
this.props.onConfirm(item)
}
handleCancel = item => () => {
this.props.onCancel(item)
}
render() {
const {item, buttonSize, isDisabled} = this.props
return (
<div className="confirm-buttons">
<button
className={classnames('btn btn-info btn-square', {
[buttonSize]: buttonSize,
})}
onClick={this.handleCancel(item)}
>
<span className="icon remove" />
</button>
<button
className={classnames('btn btn-success btn-square', {
[buttonSize]: buttonSize,
})}
disabled={isDisabled}
title={isDisabled ? 'Cannot Save' : 'Save'}
onClick={this.handleConfirm(item)}
>
<span className="icon checkmark" />
</button>
</div>
)
}
}
const {func, oneOfType, shape, string, bool} = PropTypes

View File

@ -14,9 +14,9 @@ const ContextMenu = OnClickOutside(
<span className="icon caret-down" />
</button>
<ul className="dash-graph--options-menu">
<li onClick={() => onEdit(cell)}>Edit</li>
<li onClick={onEdit(cell)}>Edit</li>
<li onClick={onRename(cell.x, cell.y, cell.isEditing)}>Rename</li>
<li onClick={() => onDelete(cell)}>Delete</li>
<li onClick={onDelete(cell)}>Delete</li>
</ul>
</div>
)

View File

@ -2,12 +2,15 @@ import React, {PropTypes, Component} from 'react'
import rome from 'rome'
import moment from 'moment'
import shortcuts from 'hson!shared/data/timeRangeShortcuts.hson'
class CustomTimeRange extends Component {
constructor(props) {
super(props)
this.handleClick = ::this.handleClick
this._formatTimeRange = ::this._formatTimeRange
this.handleTimeRangeShortcut = ::this.handleTimeRangeShortcut
}
componentDidMount() {
@ -39,15 +42,29 @@ class CustomTimeRange extends Component {
render() {
return (
<div className="custom-time--container">
<div className="custom-time--dates">
<div className="custom-time--lower" ref={r => (this.lower = r)} />
<div className="custom-time--upper" ref={r => (this.upper = r)} />
<div className="custom-time--shortcuts">
<div className="custom-time--shortcuts-header">Shortcuts</div>
{shortcuts.map(({id, name}) =>
<div
key={id}
className="custom-time--shortcut"
onClick={this.handleTimeRangeShortcut(id)}
>
{name}
</div>
)}
</div>
<div
className="custom-time--apply btn btn-sm btn-primary"
onClick={this.handleClick}
>
Apply
<div className="custom-time--wrap">
<div className="custom-time--dates">
<div className="custom-time--lower" ref={r => (this.lower = r)} />
<div className="custom-time--upper" ref={r => (this.upper = r)} />
</div>
<div
className="custom-time--apply btn btn-sm btn-primary"
onClick={this.handleClick}
>
Apply
</div>
</div>
</div>
)
@ -78,10 +95,48 @@ class CustomTimeRange extends Component {
const upper = this.upperCal.getDate().toISOString()
onApplyTimeRange({lower, upper})
if (onClose) {
onClose()
}
}
handleTimeRangeShortcut(shortcut) {
return () => {
let lower
const upper = moment()
switch (shortcut) {
case 'pastWeek': {
lower = moment().subtract(1, 'week')
break
}
case 'pastMonth': {
lower = moment().subtract(1, 'month')
break
}
case 'pastYear': {
lower = moment().subtract(1, 'year')
break
}
case 'thisWeek': {
lower = moment().startOf('week')
break
}
case 'thisMonth': {
lower = moment().startOf('month')
break
}
case 'thisYear': {
lower = moment().startOf('year')
break
}
}
this.lowerCal.setValue(lower)
this.upperCal.setValue(upper)
}
}
}
const {func, shape, string} = PropTypes

View File

@ -11,8 +11,6 @@ class DatabaseDropdown extends Component {
this.state = {
databases: [],
}
this._getDatabases = ::this._getDatabases
}
componentDidMount() {
@ -32,12 +30,12 @@ class DatabaseDropdown extends Component {
items={databases.map(text => ({text}))}
selected={database || 'Loading...'}
onChoose={onSelectDatabase}
onClick={onStartEdit ? () => onStartEdit(null) : null}
onClick={onStartEdit ? onStartEdit : null}
/>
)
}
async _getDatabases() {
_getDatabases = async () => {
const {source} = this.context
const {database, onSelectDatabase, onErrorThrown} = this.props
const proxy = source.links.proxy

View File

@ -14,16 +14,6 @@ class Dropdown extends Component {
filteredItems: this.props.items,
highlightedItemIndex: null,
}
this.handleClickOutside = ::this.handleClickOutside
this.handleClick = ::this.handleClick
this.handleSelection = ::this.handleSelection
this.toggleMenu = ::this.toggleMenu
this.handleAction = ::this.handleAction
this.handleFilterChange = ::this.handleFilterChange
this.applyFilter = ::this.applyFilter
this.handleFilterKeyPress = ::this.handleFilterKeyPress
this.handleHighlight = ::this.handleHighlight
}
static defaultProps = {
@ -34,27 +24,27 @@ class Dropdown extends Component {
useAutoComplete: false,
}
handleClickOutside() {
handleClickOutside = () => {
this.setState({isOpen: false})
}
handleClick(e) {
handleClick = e => {
this.toggleMenu(e)
if (this.props.onClick) {
this.props.onClick(e)
}
}
handleSelection(item) {
handleSelection = item => () => {
this.toggleMenu()
this.props.onChoose(item)
}
handleHighlight(itemIndex) {
handleHighlight = itemIndex => () => {
this.setState({highlightedItemIndex: itemIndex})
}
toggleMenu(e) {
toggleMenu = e => {
if (e) {
e.stopPropagation()
}
@ -68,12 +58,12 @@ class Dropdown extends Component {
this.setState({isOpen: !this.state.isOpen})
}
handleAction(e, action, item) {
handleAction = (action, item) => e => {
e.stopPropagation()
action.handler(item)
}
handleFilterKeyPress(e) {
handleFilterKeyPress = e => {
const {filteredItems, highlightedItemIndex} = this.state
if (e.key === 'Enter' && filteredItems.length) {
@ -96,7 +86,7 @@ class Dropdown extends Component {
}
}
handleFilterChange(e) {
handleFilterChange = e => {
if (e.target.value === null || e.target.value === '') {
this.setState({
searchTerm: '',
@ -110,7 +100,7 @@ class Dropdown extends Component {
}
}
applyFilter(searchTerm) {
applyFilter = searchTerm => {
const {items} = this.props
const filterText = searchTerm.toLowerCase()
const matchingItems = items.filter(item =>
@ -169,8 +159,8 @@ class Dropdown extends Component {
>
<a
href="#"
onClick={() => this.handleSelection(item)}
onMouseOver={() => this.handleHighlight(i)}
onClick={this.handleSelection(item)}
onMouseOver={this.handleHighlight(i)}
>
{item.text}
</a>
@ -181,7 +171,7 @@ class Dropdown extends Component {
<button
key={action.text}
className="dropdown-action"
onClick={e => this.handleAction(e, action, item)}
onClick={this.handleAction(action, item)}
>
<span
title={action.text}

View File

@ -33,16 +33,6 @@ export default class Dygraph extends Component {
isSnipped: false,
isFilterVisible: false,
}
this.sync = ::this.sync
this.getTimeSeries = ::this.getTimeSeries
this.handleSortLegend = ::this.handleSortLegend
this.handleLegendInputChange = ::this.handleLegendInputChange
this.handleSnipLabel = ::this.handleSnipLabel
this.handleHideLegend = ::this.handleHideLegend
this.handleToggleFilter = ::this.handleToggleFilter
this.visibility = ::this.visibility
this.getLabel = ::this.getLabel
}
static defaultProps = {
@ -52,25 +42,6 @@ export default class Dygraph extends Component {
dygraphRef: () => {},
}
getTimeSeries() {
const {timeSeries} = this.props
// Avoid 'Can't plot empty data set' errors by falling back to a
// default dataset that's valid for Dygraph.
return timeSeries.length ? timeSeries : [[0]]
}
getLabel(axis) {
const {axes, queries} = this.props
const label = _.get(axes, [axis, 'label'], '')
const queryConfig = _.get(queries, ['0', 'queryConfig'], false)
if (label || !queryConfig) {
return label
}
return buildDefaultYLabel(queryConfig)
}
componentDidMount() {
const timeSeries = this.getTimeSeries()
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
@ -248,22 +219,6 @@ export default class Dygraph extends Component {
return shallowCompare(this, nextProps, nextState)
}
visibility() {
const timeSeries = this.getTimeSeries()
const {filterText, legend} = this.state
const series = _.get(timeSeries, '0', [])
const numSeries = series.length
return Array(numSeries ? numSeries - 1 : numSeries)
.fill(true)
.map((s, i) => {
if (!legend.series[i]) {
return true
}
return !!legend.series[i].label.match(filterText)
})
}
componentDidUpdate() {
const {
labels,
@ -325,33 +280,33 @@ export default class Dygraph extends Component {
this.props.setResolution(w)
}
sync() {
sync = () => {
if (!this.state.isSynced) {
this.props.synchronizer(this.dygraph)
this.setState({isSynced: true})
}
}
handleSortLegend(sortType) {
handleSortLegend = sortType => () => {
this.setState({sortType, isAscending: !this.state.isAscending})
}
handleLegendInputChange(e) {
handleLegendInputChange = e => {
this.setState({filterText: e.target.value})
}
handleSnipLabel() {
handleSnipLabel = () => {
this.setState({isSnipped: !this.state.isSnipped})
}
handleToggleFilter() {
handleToggleFilter = () => {
this.setState({
isFilterVisible: !this.state.isFilterVisible,
filterText: '',
})
}
handleHideLegend(e) {
handleHideLegend = e => {
const {top, bottom, left, right} = this.graphRef.getBoundingClientRect()
const mouseY = e.clientY
@ -369,6 +324,43 @@ export default class Dygraph extends Component {
}
}
visibility = () => {
const timeSeries = this.getTimeSeries()
const {filterText, legend} = this.state
const series = _.get(timeSeries, '0', [])
const numSeries = series.length
return Array(numSeries ? numSeries - 1 : numSeries)
.fill(true)
.map((s, i) => {
if (!legend.series[i]) {
return true
}
return !!legend.series[i].label.match(filterText)
})
}
getTimeSeries = () => {
const {timeSeries} = this.props
// Avoid 'Can't plot empty data set' errors by falling back to a
// default dataset that's valid for Dygraph.
return timeSeries.length ? timeSeries : [[0]]
}
getLabel = axis => {
const {axes, queries} = this.props
const label = _.get(axes, [axis, 'label'], '')
const queryConfig = _.get(queries, ['0', 'queryConfig'], false)
if (label || !queryConfig) {
return label
}
return buildDefaultYLabel(queryConfig)
}
handleLegendRef = el => (this.legendRef = el)
render() {
const {
legend,
@ -393,7 +385,7 @@ export default class Dygraph extends Component {
isAscending={isAscending}
onSnip={this.handleSnipLabel}
onSort={this.handleSortLegend}
legendRef={el => (this.legendRef = el)}
legendRef={this.handleLegendRef}
onInputChange={this.handleLegendInputChange}
onToggleFilter={this.handleToggleFilter}
/>

View File

@ -39,7 +39,7 @@ const DygraphLegend = ({
'sort-btn--asc': isAscending && sortType !== 'numeric',
'sort-btn--desc': !isAscending && sortType !== 'numeric',
})}
onClick={() => onSort('alphabetic')}
onClick={onSort('alphabetic')}
>
<div className="sort-btn--arrow" />
<div className="sort-btn--top">A</div>
@ -54,7 +54,7 @@ const DygraphLegend = ({
'sort-btn--asc': isAscending && sortType === 'numeric',
'sort-btn--desc': !isAscending && sortType === 'numeric',
})}
onClick={() => onSort('numeric')}
onClick={onSort('numeric')}
>
<div className="sort-btn--arrow" />
<div className="sort-btn--top">0</div>

View File

@ -12,6 +12,10 @@ class FancyScrollbar extends Component {
autoHeight: false,
}
handleMakeDiv = className => props => {
return <div {...props} className={`fancy-scroll--${className}`} />
}
render() {
const {autoHide, autoHeight, children, className, maxHeight} = this.props
@ -25,15 +29,11 @@ class FancyScrollbar extends Component {
autoHideDuration={250}
autoHeight={autoHeight}
autoHeightMax={maxHeight}
renderTrackHorizontal={props =>
<div {...props} className="fancy-scroll--track-h" />}
renderTrackVertical={props =>
<div {...props} className="fancy-scroll--track-v" />}
renderThumbHorizontal={props =>
<div {...props} className="fancy-scroll--thumb-h" />}
renderThumbVertical={props =>
<div {...props} className="fancy-scroll--thumb-v" />}
renderView={props => <div {...props} className="fancy-scroll--view" />}
renderTrackHorizontal={this.handleMakeDiv('track-h')}
renderTrackVertical={this.handleMakeDiv('track-v')}
renderThumbHorizontal={this.handleMakeDiv('thumb-h')}
renderThumbVertical={this.handleMakeDiv('thumb-v')}
renderView={this.handleMakeDiv('view')}
>
{children}
</Scrollbars>

View File

@ -38,7 +38,7 @@ class MultiSelectDropdown extends Component {
this.setState({isOpen: false})
}
toggleMenu(e) {
toggleMenu = e => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
@ -76,7 +76,7 @@ class MultiSelectDropdown extends Component {
return (
<div className={classnames(`dropdown ${customClass}`, {open: isOpen})}>
<div
onClick={::this.toggleMenu}
onClick={this.toggleMenu}
className={`dropdown-toggle btn ${buttonSize} ${buttonColor}`}
>
{iconName ? <span className={`icon ${iconName}`} /> : null}

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import NameableGraphHeader from 'shared/components/NameableGraphHeader'
import ContextMenu from 'shared/components/ContextMenu'
@ -12,41 +13,42 @@ class NameableGraph extends Component {
}
}
toggleMenu() {
toggleMenu = () => {
this.setState({
isMenuOpen: !this.state.isMenuOpen,
})
}
handleRenameCell(e) {
handleRenameCell = e => {
const cellName = e.target.value
this.setState({cellName})
}
handleCancelEdit(cellID) {
handleCancelEdit = cellID => {
const {cell, onCancelEditCell} = this.props
this.setState({cellName: cell.name})
onCancelEditCell(cellID)
}
closeMenu() {
closeMenu = () => {
this.setState({
isMenuOpen: false,
})
}
handleDeleteCell = cell => () => {
this.props.onDeleteCell(cell)
}
handleSummonOverlay = cell => () => {
this.props.onSummonOverlayTechnologies(cell)
}
render() {
const {
cell,
onEditCell,
onUpdateCell,
onDeleteCell,
onSummonOverlayTechnologies,
isEditable,
children,
} = this.props
const {cell, children, isEditable, onEditCell, onUpdateCell} = this.props
const {cellName, isMenuOpen} = this.state
const queries = _.get(cell, ['queries'], [])
return (
<div className="dash-graph">
@ -55,26 +57,26 @@ class NameableGraph extends Component {
cellName={cellName}
isEditable={isEditable}
onUpdateCell={onUpdateCell}
onRenameCell={::this.handleRenameCell}
onCancelEditCell={::this.handleCancelEdit}
onRenameCell={this.handleRenameCell}
onCancelEditCell={this.handleCancelEdit}
/>
<ContextMenu
cell={cell}
onDelete={onDeleteCell}
onDelete={this.handleDeleteCell}
onRename={!cell.isEditing && isEditable ? onEditCell : () => {}}
toggleMenu={::this.toggleMenu}
toggleMenu={this.toggleMenu}
isOpen={isMenuOpen}
isEditable={isEditable}
handleClickOutside={::this.closeMenu}
onEdit={onSummonOverlayTechnologies}
handleClickOutside={this.closeMenu}
onEdit={this.handleSummonOverlay}
/>
<div className="dash-graph--container">
{cell.queries.length
{queries.length
? children
: <div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"
onClick={() => onSummonOverlayTechnologies(cell)}
onClick={this.handleSummonOverlay(cell)}
>
Add Graph
</button>

View File

@ -41,15 +41,15 @@ class Notifications extends Component {
)
}
renderDismiss(type) {
const {dismissNotification} = this.props
handleDismiss = type => () => this.props.dismissNotification(type)
renderDismiss(type) {
return (
<button
className="close"
data-dismiss="alert"
aria-label="Close"
onClick={() => dismissNotification(type)}
onClick={this.handleDismiss(type)}
>
<span className="icon remove" />
</button>

View File

@ -19,77 +19,57 @@ class OptIn extends Component {
this.id = uuid.v4()
this.isCustomValueInputFocused = false
this.useFixedValue = ::this.useFixedValue
this.useCustomValue = ::this.useCustomValue
this.considerResetCustomValue = ::this.considerResetCustomValue
this.setCustomValue = ::this.setCustomValue
this.setValue = ::this.setValue
}
useFixedValue() {
useFixedValue = () => {
this.setState({useCustomValue: false, customValue: ''}, () =>
this.setValue()
)
}
useCustomValue() {
useCustomValue = () => {
this.setState({useCustomValue: true}, () => this.setValue())
}
handleClickFixedValueField() {
return () => this.useFixedValue()
}
handleClickToggle() {
return () => {
const useCustomValueNext = !this.state.useCustomValue
if (useCustomValueNext) {
this.useCustomValue()
this.customValueInput.focus()
} else {
this.useFixedValue()
}
}
}
handleFocusCustomValueInput() {
return () => {
this.isCustomValueInputFocused = true
handleClickToggle = () => {
const useCustomValueNext = !this.state.useCustomValue
if (useCustomValueNext) {
this.useCustomValue()
this.customValueInput.focus()
} else {
this.useFixedValue()
}
}
handleChangeCustomValue() {
return e => {
this.setCustomValue(e.target.value)
}
handleFocusCustomValueInput = () => {
this.isCustomValueInputFocused = true
this.useCustomValue()
}
handleKeyDownCustomValueInput() {
return e => {
if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Enter') {
this.customValueInput.blur()
}
this.considerResetCustomValue()
handleChangeCustomValue = e => {
this.setCustomValue(e.target.value)
}
handleKeyDownCustomValueInput = e => {
if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Enter') {
this.customValueInput.blur()
}
this.considerResetCustomValue()
}
}
handleClickOutsideCustomValueInput() {
return e => {
if (
e.target.id !== this.grooveKnob.id &&
e.target.id !== this.grooveKnobContainer.id &&
this.isCustomValueInputFocused
) {
this.considerResetCustomValue()
}
handleClickOutsideInput = e => {
if (
e.target.id !== this.grooveKnob.id &&
e.target.id !== this.grooveKnobContainer.id &&
this.isCustomValueInputFocused
) {
this.considerResetCustomValue()
}
}
considerResetCustomValue() {
considerResetCustomValue = () => {
const customValue = this.customValueInput.value.trim()
this.setState({customValue})
@ -101,11 +81,11 @@ class OptIn extends Component {
this.isCustomValueInputFocused = false
}
setCustomValue(value) {
setCustomValue = value => {
this.setState({customValue: value}, this.setValue)
}
setValue() {
setValue = () => {
const {onSetValue} = this.props
const {useCustomValue, fixedValue, customValue} = this.state
@ -116,6 +96,8 @@ class OptIn extends Component {
}
}
handleInputRef = el => (this.customValueInput = el)
render() {
const {fixedPlaceholder, customPlaceholder, type} = this.props
const {useCustomValue, customValue} = this.state
@ -129,20 +111,20 @@ class OptIn extends Component {
<ClickOutsideInput
id={this.id}
type={type}
customPlaceholder={customPlaceholder}
customValue={customValue}
onGetRef={el => (this.customValueInput = el)}
onFocus={this.handleFocusCustomValueInput()}
onChange={this.handleChangeCustomValue()}
onKeyDown={this.handleKeyDownCustomValueInput()}
handleClickOutsideCustomValueInput={this.handleClickOutsideCustomValueInput()}
onGetRef={this.handleInputRef}
customPlaceholder={customPlaceholder}
onChange={this.handleChangeCustomValue}
onFocus={this.handleFocusCustomValueInput}
onKeyDown={this.handleKeyDownCustomValueInput}
handleClickOutsideInput={this.handleClickOutsideInput}
/>
<div
className="opt-in--groove-knob-container"
id={this.id}
ref={el => (this.grooveKnobContainer = el)}
onClick={this.handleClickToggle()}
onClick={this.handleClickToggle}
>
<div
className="opt-in--groove-knob"
@ -150,10 +132,7 @@ class OptIn extends Component {
ref={el => (this.grooveKnob = el)}
/>
</div>
<div
className="opt-in--left-label"
onClick={this.handleClickFixedValueField()}
>
<div className="opt-in--left-label" onClick={this.useFixedValue}>
{fixedPlaceholder}
</div>
</div>

View File

@ -14,10 +14,8 @@ const TemplateDrawer = ({
className={classnames('template-drawer--item', {
'template-drawer--selected': t.tempVar === selected.tempVar,
})}
onMouseOver={() => {
onMouseOverTempVar(t)
}}
onClick={() => onClickTempVar(t)}
onMouseOver={onMouseOverTempVar(t)}
onClick={onClickTempVar(t)}
key={t.tempVar}
>
{' '}{t.tempVar}{' '}

View File

@ -31,17 +31,9 @@ class TimeRangeDropdown extends Component {
upper: '',
},
}
this.findTimeRangeInputValue = ::this.findTimeRangeInputValue
this.handleSelection = ::this.handleSelection
this.toggleMenu = ::this.toggleMenu
this.showCustomTimeRange = ::this.showCustomTimeRange
this.handleApplyCustomTimeRange = ::this.handleApplyCustomTimeRange
this.handleToggleCustomTimeRange = ::this.handleToggleCustomTimeRange
this.handleCloseCustomTimeRange = ::this.handleCloseCustomTimeRange
}
findTimeRangeInputValue({upper, lower}) {
findTimeRangeInputValue = ({upper, lower}) => {
if (upper && lower) {
const format = t =>
moment(t.replace(/\'/g, '')).format('YYYY-MM-DD HH:mm')
@ -52,46 +44,53 @@ class TimeRangeDropdown extends Component {
return selected ? selected.inputValue : 'Custom'
}
handleClickOutside() {
handleClickOutside = () => {
this.setState({isOpen: false})
}
handleSelection(timeRange) {
handleSelection = timeRange => () => {
this.props.onChooseTimeRange(timeRange)
this.setState({isOpen: false})
}
toggleMenu() {
toggleMenu = () => {
this.setState({isOpen: !this.state.isOpen})
}
showCustomTimeRange() {
showCustomTimeRange = () => {
this.setState({isCustomTimeRangeOpen: true})
}
handleApplyCustomTimeRange(customTimeRange) {
this.setState({customTimeRange})
this.handleSelection({...customTimeRange})
handleApplyCustomTimeRange = customTimeRange => {
this.props.onChooseTimeRange({...customTimeRange})
this.setState({customTimeRange, isOpen: false})
}
handleToggleCustomTimeRange() {
handleToggleCustomTimeRange = () => {
this.setState({isCustomTimeRangeOpen: !this.state.isCustomTimeRangeOpen})
}
handleCloseCustomTimeRange() {
handleCloseCustomTimeRange = () => {
this.setState({isCustomTimeRangeOpen: false})
}
render() {
const {selected, preventCustomTimeRange} = this.props
const {isOpen, customTimeRange, isCustomTimeRangeOpen} = this.state
const isRelativeTimeRange = selected.upper === null
return (
<div className="time-range-dropdown">
<div className={classnames('dropdown dropdown-160', {open: isOpen})}>
<div
className={classnames('dropdown', {
'dropdown-160': isRelativeTimeRange,
'dropdown-290': !isRelativeTimeRange,
open: isOpen,
})}
>
<div
className="btn btn-sm btn-default dropdown-toggle"
onClick={() => this.toggleMenu()}
onClick={this.toggleMenu}
>
<span className="icon clock" />
<span className="dropdown-selected">
@ -105,24 +104,29 @@ class TimeRangeDropdown extends Component {
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
<li className="dropdown-header">Time Range</li>
{preventCustomTimeRange
? null
: <li
className={
isCustomTimeRangeOpen
? 'active dropdown-item custom-timerange'
: 'dropdown-item custom-timerange'
}
>
<a href="#" onClick={this.showCustomTimeRange}>
Custom Time Range
</a>
</li>}
: <div>
<li className="dropdown-header">Absolute Time Ranges</li>
<li
className={
isCustomTimeRangeOpen
? 'active dropdown-item custom-timerange'
: 'dropdown-item custom-timerange'
}
>
<a href="#" onClick={this.showCustomTimeRange}>
Custom Date Picker
</a>
</li>
</div>}
<li className="dropdown-header">
{preventCustomTimeRange ? '' : 'Relative '}Time Ranges
</li>
{timeRanges.map(item => {
return (
<li className="dropdown-item" key={item.menuOption}>
<a href="#" onClick={() => this.handleSelection(item)}>
<a href="#" onClick={this.handleSelection(item)}>
{item.menuOption}
</a>
</li>

View File

@ -0,0 +1,26 @@
[
{
"id": "pastWeek",
"name": "Past Week"
},
{
"id": "pastMonth",
"name": "Past Month"
},
{
"id": "pastYear",
"name": "Past Year"
},
{
"id": "thisWeek",
"name": "This Week"
},
{
"id": "thisMonth",
"name": "This Month"
},
{
"id": "thisYear",
"name": "This Year"
}
]

View File

@ -44,7 +44,7 @@ const kapacitorDropdown = (
buttonColor="btn-primary"
buttonSize="btn-xs"
items={kapacitorItems}
onChoose={item => setActiveKapacitor(item.kapacitor)}
onChoose={setActiveKapacitor}
addNew={{
url: `/sources/${source.id}/kapacitors/new`,
text: 'Add Kapacitor',
@ -72,12 +72,12 @@ const kapacitorDropdown = (
}
const InfluxTable = ({
sources,
source,
handleDeleteSource,
location,
router,
sources,
location,
setActiveKapacitor,
handleDeleteSource,
handleDeleteKapacitor,
}) =>
<div className="row">
@ -147,7 +147,7 @@ const InfluxTable = ({
<a
className="btn btn-xs btn-danger table--show-on-row-hover"
href="#"
onClick={() => handleDeleteSource(s)}
onClick={handleDeleteSource(s)}
>
Delete Source
</a>

View File

@ -18,8 +18,6 @@ const V_NUMBER = VERSION // eslint-disable-line no-undef
class ManageSources extends Component {
constructor(props) {
super(props)
this.handleDeleteSource = ::this.handleDeleteSource
}
componentDidMount() {
@ -36,7 +34,7 @@ class ManageSources extends Component {
}
}
handleDeleteSource(source) {
handleDeleteSource = source => () => {
const {addFlashMessage} = this.props
try {
@ -49,8 +47,12 @@ class ManageSources extends Component {
}
}
handleSetActiveKapacitor = ({kapacitor}) => {
this.props.setActiveKapacitor(kapacitor)
}
render() {
const {sources, source, setActiveKapacitor, deleteKapacitor} = this.props
const {sources, source, deleteKapacitor} = this.props
return (
<div className="page" id="manage-sources-page">
@ -67,11 +69,11 @@ class ManageSources extends Component {
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<InfluxTable
handleDeleteSource={this.handleDeleteSource}
source={source}
sources={sources}
setActiveKapacitor={setActiveKapacitor}
handleDeleteKapacitor={deleteKapacitor}
handleDeleteSource={this.handleDeleteSource}
setActiveKapacitor={this.handleSetActiveKapacitor}
/>
<p className="version-number">
Chronograf Version: {V_NUMBER}

View File

@ -47,6 +47,22 @@ export const fixtureStatusPageCells = [
y: 5,
w: 6.5,
h: 6,
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
},
{
i: 'news-feed',
@ -57,6 +73,22 @@ export const fixtureStatusPageCells = [
y: 5,
w: 3,
h: 6,
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
},
{
i: 'getting-started',
@ -67,5 +99,21 @@ export const fixtureStatusPageCells = [
y: 5,
w: 2.5,
h: 6,
queries: [
{
query: '',
queryConfig: {
database: '',
measurement: '',
retentionPolicy: '',
fields: [],
tags: {},
groupBy: {},
areTagsAccepted: false,
rawText: null,
range: null,
},
},
],
},
]

View File

@ -9,16 +9,25 @@
.custom-time--container {
display: none;
position: absolute;
flex-direction: column;
align-items: center;
flex-direction: row;
align-items: stretch;
top: 35px;
right: 0;
background: $g5-pepper;
border-radius: $radius;
padding: 8px;
z-index: 1000;
box-shadow: 0 2px 5px 0.6px rgba(15, 14, 21, 0.2);
}
.custom-time--wrap,
.custom-time--moving-dates {
display: flex;
flex-direction: column;
}
.custom-time--wrap {
padding: 8px;
align-items: center;
border-radius: 0 $radius $radius 0;
background: $g5-pepper;
}
.custom-time--dates {
display: flex;
align-items: flex-start;
@ -30,6 +39,33 @@
.custom-time--upper {
margin-left: 4px;
}
.custom-time--shortcuts {
@include no-user-select();
align-items: stretch;
background-color: $g6-smoke;
border-radius: $radius 0 0 $radius;
}
.custom-time--shortcuts-header {
white-space: nowrap;
padding: 16px;
color: $g15-platinum;
font-weight: 700;
}
.custom-time--shortcut {
white-space: nowrap;
padding: 6px 16px;
transition:
color 0.25s ease,
background-color 0.25s ease;
color: $g11-sidewalk;
font-weight: 500;
&:hover {
cursor: pointer;
background-color: $g7-graphite;
color: $g17-whisper;
}
}
$custom-time-arrow: 28px;
$rd-cell-size: 30px;
@ -222,7 +258,7 @@ $rd-cell-size: 30px;
.custom-time--container .btn.custom-time--apply {
margin-top: 8px;
width: 120px;
width: 210px;
}
/* Open State */

View File

@ -2778,6 +2778,10 @@ eslint-loader@1.6.1:
object-assign "^4.0.1"
object-hash "^1.1.4"
eslint-plugin-babel@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-4.1.2.tgz#79202a0e35757dd92780919b2336f1fa2fe53c1e"
eslint-plugin-prettier@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.1.2.tgz#4b90f4ee7f92bfbe2e926017e1ca40eb628965ea"