diff --git a/.gitignore b/.gitignore index 80332bd90c..760725f4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ chronograf*.db *_gen.go canned/apps_gen.go npm-debug.log +yarn-error.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 74db8e9343..16e11a96dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ### Features +1. [#2973](https://github.com/influxdata/chronograf/pull/2973): Add unsafe SSL to Kapacitor UI configuration + ### UI Improvements +1. [#2910](https://github.com/influxdata/chronograf/pull/2910): Redesign system notifications + ### Bug Fixes 1. [#2911](https://github.com/influxdata/chronograf/pull/2911): Fix Heroku OAuth @@ -11,6 +15,7 @@ 1. [#2947](https://github.com/influxdata/chronograf/pull/2947): Fix Okta oauth2 provider support 1. [#2866](https://github.com/influxdata/chronograf/pull/2866): Change hover text on delete mappings confirmation button to 'Delete' 1. [#2919](https://github.com/influxdata/chronograf/pull/2919): Automatically add graph type 'line' to any graph missing a type +1. [#2970](https://github.com/influxdata/chronograf/pull/2970): Fix hanging browser on docker host dashboard 1. [#3006](https://github.com/influxdata/chronograf/pull/3006): Fix Kapacitor Rules task enabled checkboxes to only toggle exactly as clicked ## v1.4.2.3 [2018-03-08] diff --git a/Makefile b/Makefile index 98f18eb239..75ffb82cfa 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ gotestrace: go test -race ./... jstest: - cd ui && yarn test + cd ui && yarn test --runInBand run: ${BINARY} ./chronograf diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index c51fe1d191..c1b330abe6 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -75,14 +75,15 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error { // MarshalServer encodes a server to binary protobuf format. func MarshalServer(s chronograf.Server) ([]byte, error) { return proto.Marshal(&Server{ - ID: int64(s.ID), - SrcID: int64(s.SrcID), - Name: s.Name, - Username: s.Username, - Password: s.Password, - URL: s.URL, - Active: s.Active, - Organization: s.Organization, + ID: int64(s.ID), + SrcID: int64(s.SrcID), + Name: s.Name, + Username: s.Username, + Password: s.Password, + URL: s.URL, + Active: s.Active, + Organization: s.Organization, + InsecureSkipVerify: s.InsecureSkipVerify, }) } @@ -101,6 +102,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error { s.URL = pb.URL s.Active = pb.Active s.Organization = pb.Organization + s.InsecureSkipVerify = pb.InsecureSkipVerify return nil } @@ -263,6 +265,27 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { } } + sortBy := &TableColumn{ + InternalName: c.TableOptions.SortBy.InternalName, + DisplayName: c.TableOptions.SortBy.DisplayName, + } + + columnNames := make([]*TableColumn, len(c.TableOptions.ColumnNames)) + for i, column := range c.TableOptions.ColumnNames { + columnNames[i] = &TableColumn{ + InternalName: column.InternalName, + DisplayName: column.DisplayName, + } + } + + tableOptions := &TableOptions{ + TimeFormat: c.TableOptions.TimeFormat, + VerticalTimeAxis: c.TableOptions.VerticalTimeAxis, + SortBy: sortBy, + Wrapping: c.TableOptions.Wrapping, + ColumnNames: columnNames, + } + cells[i] = &DashboardCell{ ID: c.ID, X: c.X, @@ -278,6 +301,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { Type: c.Legend.Type, Orientation: c.Legend.Orientation, }, + TableOptions: tableOptions, } } templates := make([]*Template, len(d.Templates)) @@ -404,6 +428,28 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { legend.Orientation = c.Legend.Orientation } + tableOptions := chronograf.TableOptions{} + if c.TableOptions != nil { + sortBy := chronograf.TableColumn{} + if c.TableOptions.SortBy != nil { + sortBy.InternalName = c.TableOptions.SortBy.InternalName + sortBy.DisplayName = c.TableOptions.SortBy.DisplayName + } + tableOptions.SortBy = sortBy + + columnNames := make([]chronograf.TableColumn, len(c.TableOptions.ColumnNames)) + for i, column := range c.TableOptions.ColumnNames { + columnNames[i] = chronograf.TableColumn{} + columnNames[i].InternalName = column.InternalName + columnNames[i].DisplayName = column.DisplayName + } + tableOptions.ColumnNames = columnNames + tableOptions.TimeFormat = c.TableOptions.TimeFormat + tableOptions.VerticalTimeAxis = c.TableOptions.VerticalTimeAxis + tableOptions.Wrapping = c.TableOptions.Wrapping + + } + // FIXME: this is merely for legacy cells and // should be removed as soon as possible cellType := c.Type @@ -412,17 +458,18 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { } cells[i] = chronograf.DashboardCell{ - ID: c.ID, - X: c.X, - Y: c.Y, - W: c.W, - H: c.H, - Name: c.Name, - Queries: queries, - Type: cellType, - Axes: axes, - CellColors: colors, - Legend: legend, + ID: c.ID, + X: c.X, + Y: c.Y, + W: c.W, + H: c.H, + Name: c.Name, + Queries: queries, + Type: cellType, + Axes: axes, + CellColors: colors, + Legend: legend, + TableOptions: tableOptions, } } diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index 3fa5963399..5e95577a5e 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -11,6 +11,8 @@ It has these top-level messages: Source Dashboard DashboardCell + TableOptions + TableColumn Color Legend Axis @@ -210,17 +212,18 @@ func (m *Dashboard) GetOrganization() string { } type DashboardCell struct { - X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"` - Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"` - 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"` - Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` - Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` - ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"` - Axes map[string]*Axis `protobuf:"bytes,9,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"` - Colors []*Color `protobuf:"bytes,10,rep,name=colors" json:"colors,omitempty"` - Legend *Legend `protobuf:"bytes,11,opt,name=legend" json:"legend,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"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` + ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"` + Axes map[string]*Axis `protobuf:"bytes,9,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"` + Colors []*Color `protobuf:"bytes,10,rep,name=colors" json:"colors,omitempty"` + Legend *Legend `protobuf:"bytes,11,opt,name=legend" json:"legend,omitempty"` + TableOptions *TableOptions `protobuf:"bytes,12,opt,name=tableOptions" json:"tableOptions,omitempty"` } func (m *DashboardCell) Reset() { *m = DashboardCell{} } @@ -305,6 +308,85 @@ func (m *DashboardCell) GetLegend() *Legend { return nil } +func (m *DashboardCell) GetTableOptions() *TableOptions { + if m != nil { + return m.TableOptions + } + return nil +} + +type TableOptions struct { + TimeFormat string `protobuf:"bytes,1,opt,name=timeFormat,proto3" json:"timeFormat,omitempty"` + VerticalTimeAxis bool `protobuf:"varint,2,opt,name=verticalTimeAxis,proto3" json:"verticalTimeAxis,omitempty"` + SortBy *TableColumn `protobuf:"bytes,3,opt,name=sortBy" json:"sortBy,omitempty"` + Wrapping string `protobuf:"bytes,4,opt,name=wrapping,proto3" json:"wrapping,omitempty"` + ColumnNames []*TableColumn `protobuf:"bytes,5,rep,name=columnNames" json:"columnNames,omitempty"` +} + +func (m *TableOptions) Reset() { *m = TableOptions{} } +func (m *TableOptions) String() string { return proto.CompactTextString(m) } +func (*TableOptions) ProtoMessage() {} +func (*TableOptions) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} } + +func (m *TableOptions) GetTimeFormat() string { + if m != nil { + return m.TimeFormat + } + return "" +} + +func (m *TableOptions) GetVerticalTimeAxis() bool { + if m != nil { + return m.VerticalTimeAxis + } + return false +} + +func (m *TableOptions) GetSortBy() *TableColumn { + if m != nil { + return m.SortBy + } + return nil +} + +func (m *TableOptions) GetWrapping() string { + if m != nil { + return m.Wrapping + } + return "" +} + +func (m *TableOptions) GetColumnNames() []*TableColumn { + if m != nil { + return m.ColumnNames + } + return nil +} + +type TableColumn struct { + InternalName string `protobuf:"bytes,1,opt,name=internalName,proto3" json:"internalName,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=displayName,proto3" json:"displayName,omitempty"` +} + +func (m *TableColumn) Reset() { *m = TableColumn{} } +func (m *TableColumn) String() string { return proto.CompactTextString(m) } +func (*TableColumn) ProtoMessage() {} +func (*TableColumn) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } + +func (m *TableColumn) GetInternalName() string { + if m != nil { + return m.InternalName + } + return "" +} + +func (m *TableColumn) GetDisplayName() string { + if m != nil { + return m.DisplayName + } + return "" +} + type Color struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` @@ -316,7 +398,7 @@ type Color struct { func (m *Color) Reset() { *m = Color{} } func (m *Color) String() string { return proto.CompactTextString(m) } func (*Color) ProtoMessage() {} -func (*Color) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} } +func (*Color) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } func (m *Color) GetID() string { if m != nil { @@ -361,7 +443,7 @@ type Legend struct { func (m *Legend) Reset() { *m = Legend{} } func (m *Legend) String() string { return proto.CompactTextString(m) } func (*Legend) ProtoMessage() {} -func (*Legend) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } +func (*Legend) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } func (m *Legend) GetType() string { if m != nil { @@ -390,7 +472,7 @@ type Axis struct { func (m *Axis) Reset() { *m = Axis{} } func (m *Axis) String() string { return proto.CompactTextString(m) } func (*Axis) ProtoMessage() {} -func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } +func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } func (m *Axis) GetLegacyBounds() []int64 { if m != nil { @@ -453,7 +535,7 @@ type Template struct { func (m *Template) Reset() { *m = Template{} } func (m *Template) String() string { return proto.CompactTextString(m) } func (*Template) ProtoMessage() {} -func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } +func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } func (m *Template) GetID() string { if m != nil { @@ -506,7 +588,7 @@ type TemplateValue struct { func (m *TemplateValue) Reset() { *m = TemplateValue{} } func (m *TemplateValue) String() string { return proto.CompactTextString(m) } func (*TemplateValue) ProtoMessage() {} -func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } +func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} } func (m *TemplateValue) GetType() string { if m != nil { @@ -541,7 +623,7 @@ type TemplateQuery struct { func (m *TemplateQuery) Reset() { *m = TemplateQuery{} } func (m *TemplateQuery) String() string { return proto.CompactTextString(m) } func (*TemplateQuery) ProtoMessage() {} -func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } +func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} } func (m *TemplateQuery) GetCommand() string { if m != nil { @@ -586,20 +668,21 @@ func (m *TemplateQuery) GetFieldKey() string { } type Server struct { - ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` - Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` - Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"` - Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"` - URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"` - SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"` - Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"` - Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"` + ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` + Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"` + Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"` + URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"` + SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"` + Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"` + Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"` + InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` } func (m *Server) Reset() { *m = Server{} } func (m *Server) String() string { return proto.CompactTextString(m) } func (*Server) ProtoMessage() {} -func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} } +func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} } func (m *Server) GetID() int64 { if m != nil { @@ -657,6 +740,13 @@ func (m *Server) GetOrganization() string { return "" } +func (m *Server) GetInsecureSkipVerify() bool { + if m != nil { + return m.InsecureSkipVerify + } + return false +} + type Layout struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"` @@ -668,7 +758,7 @@ type Layout struct { func (m *Layout) Reset() { *m = Layout{} } func (m *Layout) String() string { return proto.CompactTextString(m) } func (*Layout) ProtoMessage() {} -func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} } +func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} } func (m *Layout) GetID() string { if m != nil { @@ -722,7 +812,7 @@ type Cell struct { func (m *Cell) Reset() { *m = Cell{} } func (m *Cell) String() string { return proto.CompactTextString(m) } func (*Cell) ProtoMessage() {} -func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} } +func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} } func (m *Cell) GetX() int32 { if m != nil { @@ -816,7 +906,7 @@ type Query struct { func (m *Query) Reset() { *m = Query{} } func (m *Query) String() string { return proto.CompactTextString(m) } func (*Query) ProtoMessage() {} -func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} } +func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} } func (m *Query) GetCommand() string { if m != nil { @@ -890,7 +980,7 @@ type TimeShift struct { func (m *TimeShift) Reset() { *m = TimeShift{} } func (m *TimeShift) String() string { return proto.CompactTextString(m) } func (*TimeShift) ProtoMessage() {} -func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} } +func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} } func (m *TimeShift) GetLabel() string { if m != nil { @@ -921,7 +1011,7 @@ type Range struct { func (m *Range) Reset() { *m = Range{} } func (m *Range) String() string { return proto.CompactTextString(m) } func (*Range) ProtoMessage() {} -func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} } +func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} } func (m *Range) GetUpper() int64 { if m != nil { @@ -947,7 +1037,7 @@ type AlertRule struct { func (m *AlertRule) Reset() { *m = AlertRule{} } func (m *AlertRule) String() string { return proto.CompactTextString(m) } func (*AlertRule) ProtoMessage() {} -func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} } +func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} } func (m *AlertRule) GetID() string { if m != nil { @@ -989,7 +1079,7 @@ type User struct { func (m *User) Reset() { *m = User{} } func (m *User) String() string { return proto.CompactTextString(m) } func (*User) ProtoMessage() {} -func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} } +func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} } func (m *User) GetID() uint64 { if m != nil { @@ -1041,7 +1131,7 @@ type Role struct { func (m *Role) Reset() { *m = Role{} } func (m *Role) String() string { return proto.CompactTextString(m) } func (*Role) ProtoMessage() {} -func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} } +func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} } func (m *Role) GetOrganization() string { if m != nil { @@ -1068,7 +1158,7 @@ type Mapping struct { func (m *Mapping) Reset() { *m = Mapping{} } func (m *Mapping) String() string { return proto.CompactTextString(m) } func (*Mapping) ProtoMessage() {} -func (*Mapping) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} } +func (*Mapping) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} } func (m *Mapping) GetProvider() string { if m != nil { @@ -1114,7 +1204,7 @@ type Organization struct { func (m *Organization) Reset() { *m = Organization{} } func (m *Organization) String() string { return proto.CompactTextString(m) } func (*Organization) ProtoMessage() {} -func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} } +func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} } func (m *Organization) GetID() string { if m != nil { @@ -1144,7 +1234,7 @@ type Config struct { func (m *Config) Reset() { *m = Config{} } func (m *Config) String() string { return proto.CompactTextString(m) } func (*Config) ProtoMessage() {} -func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} } +func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{22} } func (m *Config) GetAuth() *AuthConfig { if m != nil { @@ -1160,7 +1250,7 @@ type AuthConfig struct { func (m *AuthConfig) Reset() { *m = AuthConfig{} } func (m *AuthConfig) String() string { return proto.CompactTextString(m) } func (*AuthConfig) ProtoMessage() {} -func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} } +func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{23} } func (m *AuthConfig) GetSuperAdminNewUsers() bool { if m != nil { @@ -1177,7 +1267,7 @@ type BuildInfo struct { func (m *BuildInfo) Reset() { *m = BuildInfo{} } func (m *BuildInfo) String() string { return proto.CompactTextString(m) } func (*BuildInfo) ProtoMessage() {} -func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{22} } +func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{24} } func (m *BuildInfo) GetVersion() string { if m != nil { @@ -1197,6 +1287,8 @@ func init() { proto.RegisterType((*Source)(nil), "internal.Source") proto.RegisterType((*Dashboard)(nil), "internal.Dashboard") proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell") + proto.RegisterType((*TableOptions)(nil), "internal.TableOptions") + proto.RegisterType((*TableColumn)(nil), "internal.TableColumn") proto.RegisterType((*Color)(nil), "internal.Color") proto.RegisterType((*Legend)(nil), "internal.Legend") proto.RegisterType((*Axis)(nil), "internal.Axis") @@ -1222,93 +1314,103 @@ func init() { func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } var fileDescriptorInternal = []byte{ - // 1406 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44, - 0x10, 0x97, 0x63, 0x3b, 0x89, 0x27, 0xd7, 0x52, 0x99, 0x13, 0x35, 0x45, 0x42, 0xc1, 0x02, 0x11, - 0x04, 0x3d, 0xd0, 0x55, 0x48, 0x08, 0x41, 0xa5, 0xdc, 0x05, 0x95, 0xa3, 0xd7, 0xf6, 0xba, 0xb9, - 0x3b, 0x9e, 0x50, 0xb5, 0x97, 0x4c, 0x12, 0xab, 0x8e, 0x6d, 0xd6, 0xf6, 0x5d, 0xcc, 0x87, 0x41, - 0x42, 0x82, 0x2f, 0x80, 0x78, 0xe7, 0x15, 0xf1, 0x41, 0xf8, 0x0a, 0x3c, 0x21, 0xa1, 0xd9, 0x5d, - 0xff, 0xc9, 0x25, 0xad, 0xfa, 0x80, 0x78, 0xdb, 0xdf, 0xcc, 0x66, 0x76, 0xfe, 0xfc, 0x66, 0xc6, - 0x81, 0x9b, 0x41, 0x94, 0xa1, 0x88, 0x78, 0xb8, 0x97, 0x88, 0x38, 0x8b, 0xdd, 0x6e, 0x89, 0xfd, - 0xbf, 0x5a, 0xd0, 0x1e, 0xc7, 0xb9, 0x98, 0xa0, 0x7b, 0x13, 0x5a, 0x47, 0x23, 0xcf, 0xe8, 0x1b, - 0x03, 0x93, 0xb5, 0x8e, 0x46, 0xae, 0x0b, 0xd6, 0x63, 0xbe, 0x44, 0xaf, 0xd5, 0x37, 0x06, 0x0e, - 0x93, 0x67, 0x92, 0x9d, 0x16, 0x09, 0x7a, 0xa6, 0x92, 0xd1, 0xd9, 0xbd, 0x03, 0xdd, 0xb3, 0x94, - 0xac, 0x2d, 0xd1, 0xb3, 0xa4, 0xbc, 0xc2, 0xa4, 0x3b, 0xe1, 0x69, 0x7a, 0x15, 0x8b, 0xa9, 0x67, - 0x2b, 0x5d, 0x89, 0xdd, 0x5b, 0x60, 0x9e, 0xb1, 0x63, 0xaf, 0x2d, 0xc5, 0x74, 0x74, 0x3d, 0xe8, - 0x8c, 0x70, 0xc6, 0xf3, 0x30, 0xf3, 0x3a, 0x7d, 0x63, 0xd0, 0x65, 0x25, 0x24, 0x3b, 0xa7, 0x18, - 0xe2, 0x5c, 0xf0, 0x99, 0xd7, 0x55, 0x76, 0x4a, 0xec, 0xee, 0x81, 0x7b, 0x14, 0xa5, 0x38, 0xc9, - 0x05, 0x8e, 0x9f, 0x07, 0xc9, 0x39, 0x8a, 0x60, 0x56, 0x78, 0x8e, 0x34, 0xb0, 0x45, 0x43, 0xaf, - 0x3c, 0xc2, 0x8c, 0xd3, 0xdb, 0x20, 0x4d, 0x95, 0xd0, 0xf5, 0x61, 0x67, 0xbc, 0xe0, 0x02, 0xa7, - 0x63, 0x9c, 0x08, 0xcc, 0xbc, 0x9e, 0x54, 0xaf, 0xc9, 0xe8, 0xce, 0x13, 0x31, 0xe7, 0x51, 0xf0, - 0x03, 0xcf, 0x82, 0x38, 0xf2, 0x76, 0xd4, 0x9d, 0xa6, 0x8c, 0xb2, 0xc4, 0xe2, 0x10, 0xbd, 0x1b, - 0x2a, 0x4b, 0x74, 0xf6, 0x7f, 0x33, 0xc0, 0x19, 0xf1, 0x74, 0x71, 0x11, 0x73, 0x31, 0x7d, 0xa5, - 0x5c, 0xdf, 0x05, 0x7b, 0x82, 0x61, 0x98, 0x7a, 0x66, 0xdf, 0x1c, 0xf4, 0xf6, 0x6f, 0xef, 0x55, - 0x45, 0xac, 0xec, 0x1c, 0x62, 0x18, 0x32, 0x75, 0xcb, 0xfd, 0x04, 0x9c, 0x0c, 0x97, 0x49, 0xc8, - 0x33, 0x4c, 0x3d, 0x4b, 0xfe, 0xc4, 0xad, 0x7f, 0x72, 0xaa, 0x55, 0xac, 0xbe, 0xb4, 0x11, 0x8a, - 0xbd, 0x19, 0x8a, 0xff, 0x4f, 0x0b, 0x6e, 0xac, 0x3d, 0xe7, 0xee, 0x80, 0xb1, 0x92, 0x9e, 0xdb, - 0xcc, 0x58, 0x11, 0x2a, 0xa4, 0xd7, 0x36, 0x33, 0x0a, 0x42, 0x57, 0x92, 0x1b, 0x36, 0x33, 0xae, - 0x08, 0x2d, 0x24, 0x23, 0x6c, 0x66, 0x2c, 0xdc, 0x0f, 0xa0, 0xf3, 0x7d, 0x8e, 0x22, 0xc0, 0xd4, - 0xb3, 0xa5, 0x77, 0xaf, 0xd5, 0xde, 0x3d, 0xcd, 0x51, 0x14, 0xac, 0xd4, 0x53, 0x36, 0x24, 0x9b, - 0x14, 0x35, 0xe4, 0x99, 0x64, 0x19, 0x31, 0xaf, 0xa3, 0x64, 0x74, 0xd6, 0x59, 0x54, 0x7c, 0xa0, - 0x2c, 0x7e, 0x0a, 0x16, 0x5f, 0x61, 0xea, 0x39, 0xd2, 0xfe, 0x3b, 0x2f, 0x48, 0xd8, 0xde, 0x70, - 0x85, 0xe9, 0x57, 0x51, 0x26, 0x0a, 0x26, 0xaf, 0xbb, 0xef, 0x43, 0x7b, 0x12, 0x87, 0xb1, 0x48, - 0x3d, 0xb8, 0xee, 0xd8, 0x21, 0xc9, 0x99, 0x56, 0xbb, 0x03, 0x68, 0x87, 0x38, 0xc7, 0x68, 0x2a, - 0x99, 0xd1, 0xdb, 0xbf, 0x55, 0x5f, 0x3c, 0x96, 0x72, 0xa6, 0xf5, 0x77, 0x1e, 0x80, 0x53, 0xbd, - 0x42, 0x44, 0x7f, 0x8e, 0x85, 0xcc, 0x99, 0xc3, 0xe8, 0xe8, 0xbe, 0x0b, 0xf6, 0x25, 0x0f, 0x73, - 0x55, 0xef, 0xde, 0xfe, 0xcd, 0xda, 0xce, 0x70, 0x15, 0xa4, 0x4c, 0x29, 0x3f, 0x6f, 0x7d, 0x66, - 0xf8, 0x73, 0xb0, 0xa5, 0x0f, 0x0d, 0xc6, 0x38, 0x25, 0x63, 0x64, 0x27, 0xb6, 0x1a, 0x9d, 0x78, - 0x0b, 0xcc, 0xaf, 0x71, 0xa5, 0x9b, 0x93, 0x8e, 0x15, 0xaf, 0xac, 0x06, 0xaf, 0x76, 0xc1, 0x3e, - 0x97, 0x8f, 0xab, 0x7a, 0x2b, 0xe0, 0xdf, 0x87, 0xb6, 0x8a, 0xa1, 0xb2, 0x6c, 0x34, 0x2c, 0xf7, - 0xa1, 0xf7, 0x44, 0x04, 0x18, 0x65, 0x8a, 0x29, 0xea, 0xd1, 0xa6, 0xc8, 0xff, 0xd5, 0x00, 0x8b, - 0x9c, 0x27, 0x56, 0x85, 0x38, 0xe7, 0x93, 0xe2, 0x20, 0xce, 0xa3, 0x69, 0xea, 0x19, 0x7d, 0x73, - 0x60, 0xb2, 0x35, 0x99, 0xfb, 0x06, 0xb4, 0x2f, 0x94, 0xb6, 0xd5, 0x37, 0x07, 0x0e, 0xd3, 0x88, - 0x5c, 0x0b, 0xf9, 0x05, 0x86, 0x3a, 0x04, 0x05, 0xe8, 0x76, 0x22, 0x70, 0x16, 0xac, 0x74, 0x18, - 0x1a, 0x91, 0x3c, 0xcd, 0x67, 0x24, 0x57, 0x91, 0x68, 0x44, 0x01, 0x5c, 0xf0, 0xb4, 0xa2, 0x0f, - 0x9d, 0xc9, 0x72, 0x3a, 0xe1, 0x61, 0xc9, 0x1f, 0x05, 0xfc, 0xdf, 0x0d, 0x9a, 0x2b, 0xaa, 0x1f, - 0x36, 0x32, 0xfc, 0x26, 0x74, 0xa9, 0x57, 0x9e, 0x5d, 0x72, 0xa1, 0x03, 0xee, 0x10, 0x3e, 0xe7, - 0xc2, 0xfd, 0x18, 0xda, 0xb2, 0x44, 0x5b, 0x7a, 0xb3, 0x34, 0x27, 0xb3, 0xca, 0xf4, 0xb5, 0x8a, - 0xbd, 0x56, 0x83, 0xbd, 0x55, 0xb0, 0x76, 0x33, 0xd8, 0xbb, 0x60, 0x53, 0x1b, 0x14, 0xd2, 0xfb, - 0xad, 0x96, 0x55, 0xb3, 0xa8, 0x5b, 0xfe, 0x19, 0xdc, 0x58, 0x7b, 0xb1, 0x7a, 0xc9, 0x58, 0x7f, - 0xa9, 0xa6, 0x9b, 0xa3, 0xe9, 0x45, 0x33, 0x35, 0xc5, 0x10, 0x27, 0x19, 0x4e, 0x65, 0xbe, 0xbb, - 0xac, 0xc2, 0xfe, 0x4f, 0x46, 0x6d, 0x57, 0xbe, 0x47, 0x53, 0x73, 0x12, 0x2f, 0x97, 0x3c, 0x9a, - 0x6a, 0xd3, 0x25, 0xa4, 0xbc, 0x4d, 0x2f, 0xb4, 0xe9, 0xd6, 0xf4, 0x82, 0xb0, 0x48, 0x74, 0x05, - 0x5b, 0x22, 0x21, 0xee, 0x2c, 0x91, 0xa7, 0xb9, 0xc0, 0x25, 0x46, 0x99, 0x4e, 0x41, 0x53, 0xe4, - 0xde, 0x86, 0x4e, 0xc6, 0xe7, 0xcf, 0xa8, 0x49, 0x74, 0x25, 0x33, 0x3e, 0x7f, 0x88, 0x85, 0xfb, - 0x16, 0x38, 0xb3, 0x00, 0xc3, 0xa9, 0x54, 0xa9, 0x72, 0x76, 0xa5, 0xe0, 0x21, 0x16, 0xfe, 0x1f, - 0x06, 0xb4, 0xc7, 0x28, 0x2e, 0x51, 0xbc, 0xd2, 0x38, 0x6d, 0xae, 0x29, 0xf3, 0x25, 0x6b, 0xca, - 0xda, 0xbe, 0xa6, 0xec, 0x7a, 0x4d, 0xed, 0x82, 0x3d, 0x16, 0x93, 0xa3, 0x91, 0xf4, 0xc8, 0x64, - 0x0a, 0x10, 0x1b, 0x87, 0x93, 0x2c, 0xb8, 0x44, 0xbd, 0xbb, 0x34, 0xda, 0x98, 0xb2, 0xdd, 0x2d, - 0x53, 0xf6, 0x47, 0x03, 0xda, 0xc7, 0xbc, 0x88, 0xf3, 0x6c, 0x83, 0x85, 0x7d, 0xe8, 0x0d, 0x93, - 0x24, 0x0c, 0x26, 0x6b, 0x9d, 0xd7, 0x10, 0xd1, 0x8d, 0x47, 0x8d, 0xfc, 0xaa, 0xd8, 0x9a, 0x22, - 0x1a, 0x37, 0x87, 0x72, 0x93, 0xa8, 0xb5, 0xd0, 0x18, 0x37, 0x6a, 0x81, 0x48, 0x25, 0x25, 0x61, - 0x98, 0x67, 0xf1, 0x2c, 0x8c, 0xaf, 0x64, 0xb4, 0x5d, 0x56, 0x61, 0xff, 0xcf, 0x16, 0x58, 0xff, - 0xd7, 0xf4, 0xdf, 0x01, 0x23, 0xd0, 0xc5, 0x36, 0x82, 0x6a, 0x17, 0x74, 0x1a, 0xbb, 0xc0, 0x83, - 0x4e, 0x21, 0x78, 0x34, 0xc7, 0xd4, 0xeb, 0xca, 0xe9, 0x52, 0x42, 0xa9, 0x91, 0x7d, 0xa4, 0x96, - 0x80, 0xc3, 0x4a, 0x58, 0xf5, 0x05, 0x34, 0xfa, 0xe2, 0x23, 0xbd, 0x2f, 0x7a, 0xd2, 0x23, 0x6f, - 0x3d, 0x2d, 0xd7, 0xd7, 0xc4, 0x7f, 0x37, 0xd3, 0xff, 0x36, 0xc0, 0xae, 0x9a, 0xea, 0x70, 0xbd, - 0xa9, 0x0e, 0xeb, 0xa6, 0x1a, 0x1d, 0x94, 0x4d, 0x35, 0x3a, 0x20, 0xcc, 0x4e, 0xca, 0xa6, 0x62, - 0x27, 0x54, 0xac, 0x07, 0x22, 0xce, 0x93, 0x83, 0x42, 0x55, 0xd5, 0x61, 0x15, 0x26, 0x26, 0x7e, - 0xbb, 0x40, 0xa1, 0x53, 0xed, 0x30, 0x8d, 0x88, 0xb7, 0xc7, 0x72, 0xe0, 0xa8, 0xe4, 0x2a, 0xe0, - 0xbe, 0x07, 0x36, 0xa3, 0xe4, 0xc9, 0x0c, 0xaf, 0xd5, 0x45, 0x8a, 0x99, 0xd2, 0x92, 0x51, 0xf5, - 0x9d, 0xa8, 0x09, 0x5c, 0x7e, 0x35, 0x7e, 0x08, 0xed, 0xf1, 0x22, 0x98, 0x65, 0xe5, 0xd6, 0x7d, - 0xbd, 0x31, 0xb0, 0x82, 0x25, 0x4a, 0x1d, 0xd3, 0x57, 0xfc, 0xa7, 0xe0, 0x54, 0xc2, 0xda, 0x1d, - 0xa3, 0xe9, 0x8e, 0x0b, 0xd6, 0x59, 0x14, 0x64, 0x65, 0xeb, 0xd2, 0x99, 0x82, 0x7d, 0x9a, 0xf3, - 0x28, 0x0b, 0xb2, 0xa2, 0x6c, 0xdd, 0x12, 0xfb, 0xf7, 0xb4, 0xfb, 0x64, 0xee, 0x2c, 0x49, 0x50, - 0xe8, 0x31, 0xa0, 0x80, 0x7c, 0x24, 0xbe, 0x42, 0x35, 0xc1, 0x4d, 0xa6, 0x80, 0xff, 0x1d, 0x38, - 0xc3, 0x10, 0x45, 0xc6, 0xf2, 0x10, 0xb7, 0x6d, 0xd6, 0x6f, 0xc6, 0x4f, 0x1e, 0x97, 0x1e, 0xd0, - 0xb9, 0x6e, 0x79, 0xf3, 0x5a, 0xcb, 0x3f, 0xe4, 0x09, 0x3f, 0x1a, 0x49, 0x9e, 0x9b, 0x4c, 0x23, - 0xff, 0x67, 0x03, 0x2c, 0x9a, 0x2d, 0x0d, 0xd3, 0xd6, 0xcb, 0xe6, 0xd2, 0x89, 0x88, 0x2f, 0x83, - 0x29, 0x8a, 0x32, 0xb8, 0x12, 0xcb, 0xa4, 0x4f, 0x16, 0x58, 0x2d, 0x70, 0x8d, 0x88, 0x6b, 0xf4, - 0x51, 0x59, 0xf6, 0x52, 0x83, 0x6b, 0x24, 0x66, 0x4a, 0xe9, 0xbe, 0x0d, 0x30, 0xce, 0x13, 0x14, - 0xc3, 0xe9, 0x32, 0x88, 0x64, 0xd1, 0xbb, 0xac, 0x21, 0xf1, 0xef, 0xab, 0xcf, 0xd4, 0x8d, 0x09, - 0x65, 0x6c, 0xff, 0xa4, 0xbd, 0xee, 0xb9, 0xff, 0x8b, 0x01, 0x9d, 0x47, 0x3c, 0x49, 0x82, 0x68, - 0xbe, 0x16, 0x85, 0xf1, 0xc2, 0x28, 0x5a, 0x6b, 0x51, 0xec, 0xc3, 0x6e, 0x79, 0x67, 0xed, 0x7d, - 0x95, 0x85, 0xad, 0x3a, 0x9d, 0x51, 0xab, 0x2a, 0xd6, 0xab, 0x7c, 0xc3, 0x9e, 0xae, 0xdf, 0xd9, - 0x56, 0xf0, 0x8d, 0xaa, 0xf4, 0xa1, 0xa7, 0xff, 0x7b, 0xc8, 0x2f, 0x79, 0x3d, 0x54, 0x1b, 0x22, - 0x7f, 0x1f, 0xda, 0x87, 0x71, 0x34, 0x0b, 0xe6, 0xee, 0x00, 0xac, 0x61, 0x9e, 0x2d, 0xa4, 0xc5, - 0xde, 0xfe, 0x6e, 0xa3, 0xf1, 0xf3, 0x6c, 0xa1, 0xee, 0x30, 0x79, 0xc3, 0xff, 0x02, 0xa0, 0x96, - 0xd1, 0x1f, 0x97, 0xba, 0x1a, 0x8f, 0xf1, 0x8a, 0x28, 0x93, 0x4a, 0x2b, 0x5d, 0xb6, 0x45, 0xe3, - 0x7f, 0x09, 0xce, 0x41, 0x1e, 0x84, 0xd3, 0xa3, 0x68, 0x16, 0xd3, 0xe8, 0x38, 0x47, 0x91, 0xd6, - 0xf5, 0x2a, 0x21, 0xa5, 0x9b, 0xa6, 0x48, 0xd5, 0x43, 0x1a, 0x5d, 0xb4, 0xe5, 0x7f, 0xbf, 0x7b, - 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xfe, 0xe9, 0xd1, 0x8f, 0x0d, 0x0e, 0x00, 0x00, + // 1558 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x6f, 0xdb, 0x46, + 0x12, 0x07, 0x25, 0x51, 0x12, 0x47, 0x4e, 0xce, 0xe0, 0xf9, 0x12, 0x5e, 0x0e, 0x38, 0xe8, 0x88, + 0x3b, 0x9c, 0xee, 0x4f, 0x7c, 0x07, 0x05, 0x45, 0x8b, 0xa0, 0x0d, 0x20, 0x5b, 0x6d, 0xea, 0xc6, + 0x89, 0x9d, 0x95, 0xed, 0x3e, 0x15, 0xc1, 0x4a, 0x1a, 0x49, 0x44, 0x28, 0x92, 0x5d, 0x92, 0xb6, + 0xd9, 0x0f, 0x53, 0xa0, 0x40, 0xfb, 0x05, 0x8a, 0xbe, 0xf4, 0xa9, 0xef, 0xfd, 0x10, 0x7d, 0xec, + 0x57, 0x68, 0x1f, 0x8b, 0xd9, 0x5d, 0x52, 0x2b, 0x4b, 0x09, 0x52, 0xa0, 0xe8, 0xdb, 0xfe, 0x66, + 0x86, 0xb3, 0xf3, 0x7f, 0x96, 0x70, 0x3b, 0x88, 0x32, 0x14, 0x11, 0x0f, 0xf7, 0x13, 0x11, 0x67, + 0xb1, 0xdb, 0x2e, 0xb1, 0xff, 0x63, 0x0d, 0x9a, 0xa3, 0x38, 0x17, 0x13, 0x74, 0x6f, 0x43, 0xed, + 0x68, 0xe8, 0x59, 0x5d, 0xab, 0x57, 0x67, 0xb5, 0xa3, 0xa1, 0xeb, 0x42, 0xe3, 0x19, 0x5f, 0xa2, + 0x57, 0xeb, 0x5a, 0x3d, 0x87, 0xc9, 0x33, 0xd1, 0xce, 0x8a, 0x04, 0xbd, 0xba, 0xa2, 0xd1, 0xd9, + 0xbd, 0x07, 0xed, 0xf3, 0x94, 0xb4, 0x2d, 0xd1, 0x6b, 0x48, 0x7a, 0x85, 0x89, 0x77, 0xca, 0xd3, + 0xf4, 0x2a, 0x16, 0x53, 0xcf, 0x56, 0xbc, 0x12, 0xbb, 0xbb, 0x50, 0x3f, 0x67, 0xc7, 0x5e, 0x53, + 0x92, 0xe9, 0xe8, 0x7a, 0xd0, 0x1a, 0xe2, 0x8c, 0xe7, 0x61, 0xe6, 0xb5, 0xba, 0x56, 0xaf, 0xcd, + 0x4a, 0x48, 0x7a, 0xce, 0x30, 0xc4, 0xb9, 0xe0, 0x33, 0xaf, 0xad, 0xf4, 0x94, 0xd8, 0xdd, 0x07, + 0xf7, 0x28, 0x4a, 0x71, 0x92, 0x0b, 0x1c, 0xbd, 0x0c, 0x92, 0x0b, 0x14, 0xc1, 0xac, 0xf0, 0x1c, + 0xa9, 0x60, 0x0b, 0x87, 0x6e, 0x79, 0x8a, 0x19, 0xa7, 0xbb, 0x41, 0xaa, 0x2a, 0xa1, 0xeb, 0xc3, + 0xce, 0x68, 0xc1, 0x05, 0x4e, 0x47, 0x38, 0x11, 0x98, 0x79, 0x1d, 0xc9, 0x5e, 0xa3, 0x91, 0xcc, + 0x89, 0x98, 0xf3, 0x28, 0xf8, 0x8c, 0x67, 0x41, 0x1c, 0x79, 0x3b, 0x4a, 0xc6, 0xa4, 0x51, 0x94, + 0x58, 0x1c, 0xa2, 0x77, 0x4b, 0x45, 0x89, 0xce, 0xfe, 0x37, 0x16, 0x38, 0x43, 0x9e, 0x2e, 0xc6, + 0x31, 0x17, 0xd3, 0x37, 0x8a, 0xf5, 0x7d, 0xb0, 0x27, 0x18, 0x86, 0xa9, 0x57, 0xef, 0xd6, 0x7b, + 0x9d, 0xfe, 0xdd, 0xfd, 0x2a, 0x89, 0x95, 0x9e, 0x43, 0x0c, 0x43, 0xa6, 0xa4, 0xdc, 0xff, 0x83, + 0x93, 0xe1, 0x32, 0x09, 0x79, 0x86, 0xa9, 0xd7, 0x90, 0x9f, 0xb8, 0xab, 0x4f, 0xce, 0x34, 0x8b, + 0xad, 0x84, 0x36, 0x5c, 0xb1, 0x37, 0x5d, 0xf1, 0xbf, 0xad, 0xc3, 0xad, 0xb5, 0xeb, 0xdc, 0x1d, + 0xb0, 0xae, 0xa5, 0xe5, 0x36, 0xb3, 0xae, 0x09, 0x15, 0xd2, 0x6a, 0x9b, 0x59, 0x05, 0xa1, 0x2b, + 0x59, 0x1b, 0x36, 0xb3, 0xae, 0x08, 0x2d, 0x64, 0x45, 0xd8, 0xcc, 0x5a, 0xb8, 0xff, 0x82, 0xd6, + 0xa7, 0x39, 0x8a, 0x00, 0x53, 0xcf, 0x96, 0xd6, 0xfd, 0x61, 0x65, 0xdd, 0xf3, 0x1c, 0x45, 0xc1, + 0x4a, 0x3e, 0x45, 0x43, 0x56, 0x93, 0x2a, 0x0d, 0x79, 0x26, 0x5a, 0x46, 0x95, 0xd7, 0x52, 0x34, + 0x3a, 0xeb, 0x28, 0xaa, 0x7a, 0xa0, 0x28, 0xbe, 0x05, 0x0d, 0x7e, 0x8d, 0xa9, 0xe7, 0x48, 0xfd, + 0x7f, 0x7b, 0x45, 0xc0, 0xf6, 0x07, 0xd7, 0x98, 0xbe, 0x1f, 0x65, 0xa2, 0x60, 0x52, 0xdc, 0xfd, + 0x27, 0x34, 0x27, 0x71, 0x18, 0x8b, 0xd4, 0x83, 0x9b, 0x86, 0x1d, 0x12, 0x9d, 0x69, 0xb6, 0xdb, + 0x83, 0x66, 0x88, 0x73, 0x8c, 0xa6, 0xb2, 0x32, 0x3a, 0xfd, 0xdd, 0x95, 0xe0, 0xb1, 0xa4, 0x33, + 0xcd, 0x77, 0x1f, 0xc2, 0x4e, 0xc6, 0xc7, 0x21, 0x9e, 0x24, 0x14, 0xc5, 0x54, 0x56, 0x49, 0xa7, + 0x7f, 0xc7, 0xc8, 0x87, 0xc1, 0x65, 0x6b, 0xb2, 0xf7, 0x1e, 0x83, 0x53, 0x59, 0x48, 0x4d, 0xf2, + 0x12, 0x0b, 0x19, 0x6f, 0x87, 0xd1, 0xd1, 0xfd, 0x3b, 0xd8, 0x97, 0x3c, 0xcc, 0x55, 0xad, 0x74, + 0xfa, 0xb7, 0x57, 0x3a, 0x07, 0xd7, 0x41, 0xca, 0x14, 0xf3, 0x61, 0xed, 0x1d, 0xcb, 0xff, 0xc1, + 0x82, 0x1d, 0xf3, 0x1e, 0xf7, 0xaf, 0x00, 0x59, 0xb0, 0xc4, 0x0f, 0x62, 0xb1, 0xe4, 0x99, 0xd6, + 0x69, 0x50, 0xdc, 0x7f, 0xc3, 0xee, 0x25, 0x8a, 0x2c, 0x98, 0xf0, 0xf0, 0x2c, 0x58, 0x22, 0xe9, + 0x93, 0xb7, 0xb4, 0xd9, 0x06, 0xdd, 0xbd, 0x0f, 0xcd, 0x34, 0x16, 0xd9, 0x41, 0x21, 0xf3, 0xdd, + 0xe9, 0xff, 0xe9, 0x86, 0x6f, 0x87, 0x71, 0x98, 0x2f, 0x23, 0xa6, 0x85, 0xa8, 0x81, 0xaf, 0x04, + 0x4f, 0x92, 0x20, 0x9a, 0x97, 0x43, 0xa2, 0xc4, 0xee, 0xdb, 0xd0, 0x99, 0x48, 0x69, 0x2a, 0xfb, + 0xb2, 0x3a, 0x5e, 0xa1, 0xcf, 0x94, 0xf4, 0x47, 0xd0, 0x31, 0x78, 0x54, 0xcf, 0xe5, 0x37, 0xb2, + 0x99, 0x94, 0x83, 0x6b, 0x34, 0xb7, 0x0b, 0x9d, 0x69, 0x90, 0x26, 0x21, 0x2f, 0x8c, 0x7e, 0x33, + 0x49, 0xfe, 0x1c, 0x6c, 0x99, 0x75, 0xa3, 0x47, 0x9d, 0xb2, 0x47, 0xe5, 0xec, 0xab, 0x19, 0xb3, + 0x6f, 0x17, 0xea, 0x1f, 0xe2, 0xb5, 0x1e, 0x87, 0x74, 0xac, 0x3a, 0xb9, 0x61, 0x74, 0xf2, 0x1e, + 0xd8, 0x17, 0x32, 0x65, 0xaa, 0xc3, 0x14, 0xf0, 0x1f, 0x41, 0x53, 0x55, 0x4d, 0xa5, 0xd9, 0x32, + 0x34, 0x77, 0xa1, 0x73, 0x22, 0x02, 0x8c, 0x32, 0xd5, 0x9b, 0xda, 0x50, 0x83, 0xe4, 0x7f, 0x6d, + 0x41, 0x43, 0xa6, 0xc2, 0x87, 0x9d, 0x10, 0xe7, 0x7c, 0x52, 0x1c, 0xc4, 0x79, 0x34, 0x4d, 0x3d, + 0xab, 0x5b, 0xef, 0xd5, 0xd9, 0x1a, 0xcd, 0xbd, 0x03, 0xcd, 0xb1, 0xe2, 0xd6, 0xba, 0xf5, 0x9e, + 0xc3, 0x34, 0x22, 0xd3, 0x42, 0x3e, 0xc6, 0x50, 0xbb, 0xa0, 0x00, 0x49, 0x27, 0x02, 0x67, 0xc1, + 0xb5, 0x76, 0x43, 0x23, 0xa2, 0xa7, 0xf9, 0x8c, 0xe8, 0xca, 0x13, 0x8d, 0xc8, 0x81, 0x31, 0x4f, + 0xab, 0x86, 0xa5, 0x33, 0x69, 0x4e, 0x27, 0x3c, 0x2c, 0x3b, 0x56, 0x01, 0xff, 0x3b, 0x8b, 0x26, + 0xb9, 0x9a, 0x40, 0x1b, 0x11, 0xfe, 0x33, 0xb4, 0x69, 0x3a, 0xbd, 0xb8, 0xe4, 0x42, 0x3b, 0xdc, + 0x22, 0x7c, 0xc1, 0x85, 0xfb, 0x3f, 0x68, 0xca, 0xc2, 0xde, 0x32, 0x0d, 0x4b, 0x75, 0x32, 0xaa, + 0x4c, 0x8b, 0x55, 0xf3, 0xa2, 0x61, 0xcc, 0x8b, 0xca, 0x59, 0xdb, 0x74, 0xf6, 0x3e, 0xd8, 0x34, + 0x78, 0x0a, 0x69, 0xfd, 0x56, 0xcd, 0x6a, 0x3c, 0x29, 0x29, 0xff, 0x1c, 0x6e, 0xad, 0xdd, 0x58, + 0xdd, 0x64, 0xad, 0xdf, 0xb4, 0x6a, 0x52, 0x47, 0x37, 0x25, 0x35, 0x41, 0x8a, 0x21, 0x4e, 0x32, + 0x9c, 0xca, 0x78, 0xb7, 0x59, 0x85, 0xfd, 0x2f, 0xac, 0x95, 0x5e, 0x79, 0x1f, 0xed, 0xa9, 0x49, + 0xbc, 0x5c, 0xf2, 0x68, 0xaa, 0x55, 0x97, 0x90, 0xe2, 0x36, 0x1d, 0x6b, 0xd5, 0xb5, 0xe9, 0x98, + 0xb0, 0x48, 0x74, 0x06, 0x6b, 0x22, 0xa1, 0xda, 0x59, 0x22, 0x4f, 0x73, 0x81, 0x4b, 0x8c, 0x32, + 0x1d, 0x02, 0x93, 0xe4, 0xde, 0x85, 0x56, 0xc6, 0xe7, 0x2f, 0x68, 0xb4, 0xe8, 0x4c, 0x66, 0x7c, + 0xfe, 0x04, 0x0b, 0xf7, 0x2f, 0xe0, 0xcc, 0x02, 0x0c, 0xa7, 0x92, 0xa5, 0xd2, 0xd9, 0x96, 0x84, + 0x27, 0x58, 0xf8, 0x3f, 0x5b, 0xd0, 0x1c, 0xa1, 0xb8, 0x44, 0xf1, 0x46, 0x0b, 0xcc, 0x7c, 0x18, + 0xd4, 0x5f, 0xf3, 0x30, 0x68, 0x6c, 0x7f, 0x18, 0xd8, 0xab, 0x87, 0xc1, 0x1e, 0xd8, 0x23, 0x31, + 0x39, 0x1a, 0x4a, 0x8b, 0xea, 0x4c, 0x01, 0xaa, 0xc6, 0xc1, 0x24, 0x0b, 0x2e, 0x51, 0xbf, 0x16, + 0x34, 0xda, 0xd8, 0x6b, 0xed, 0x2d, 0x2b, 0xfa, 0x57, 0x3e, 0x1a, 0xfc, 0xcf, 0x2d, 0x68, 0x1e, + 0xf3, 0x22, 0xce, 0xb3, 0x8d, 0xaa, 0xed, 0x42, 0x67, 0x90, 0x24, 0x61, 0x30, 0x59, 0xeb, 0x54, + 0x83, 0x44, 0x12, 0x4f, 0x8d, 0x7c, 0xa8, 0x58, 0x98, 0x24, 0x1a, 0xea, 0x87, 0x72, 0xd7, 0xab, + 0xc5, 0x6d, 0x0c, 0x75, 0xb5, 0xe2, 0x25, 0x93, 0x82, 0x36, 0xc8, 0xb3, 0x78, 0x16, 0xc6, 0x57, + 0x32, 0x3a, 0x6d, 0x56, 0x61, 0xff, 0xfb, 0x1a, 0x34, 0x7e, 0xaf, 0xfd, 0xbc, 0x03, 0x56, 0xa0, + 0x8b, 0xc3, 0x0a, 0xaa, 0x6d, 0xdd, 0x32, 0xb6, 0xb5, 0x07, 0xad, 0x42, 0xf0, 0x68, 0x8e, 0xa9, + 0xd7, 0x96, 0xd3, 0xa8, 0x84, 0x92, 0x23, 0xfb, 0x4e, 0xad, 0x69, 0x87, 0x95, 0xb0, 0xea, 0x23, + 0x30, 0xfa, 0xe8, 0xbf, 0x7a, 0xa3, 0x77, 0xa4, 0x45, 0xde, 0x7a, 0x58, 0x6e, 0x2e, 0xf2, 0xdf, + 0x6e, 0x73, 0xfe, 0x64, 0x81, 0x5d, 0x35, 0xe1, 0xe1, 0x7a, 0x13, 0x1e, 0xae, 0x9a, 0x70, 0x78, + 0x50, 0x36, 0xe1, 0xf0, 0x80, 0x30, 0x3b, 0x2d, 0x9b, 0x90, 0x9d, 0x52, 0xb2, 0x1e, 0x8b, 0x38, + 0x4f, 0x0e, 0x0a, 0x95, 0x55, 0x87, 0x55, 0x98, 0x2a, 0xf7, 0xe3, 0x05, 0x0a, 0x1d, 0x6a, 0x87, + 0x69, 0x44, 0x75, 0x7e, 0x2c, 0x07, 0x94, 0x0a, 0xae, 0x02, 0xee, 0x3f, 0xc0, 0x66, 0x14, 0x3c, + 0x19, 0xe1, 0xb5, 0xbc, 0x48, 0x32, 0x53, 0x5c, 0x52, 0xaa, 0x5e, 0xf2, 0xba, 0xe0, 0xcb, 0x77, + 0xfd, 0x7f, 0xa0, 0x39, 0x5a, 0x04, 0xb3, 0xac, 0x7c, 0x17, 0xfd, 0xd1, 0x18, 0x70, 0xc1, 0x12, + 0x25, 0x8f, 0x69, 0x11, 0xff, 0x39, 0x38, 0x15, 0x71, 0x65, 0x8e, 0x65, 0x9a, 0xe3, 0x42, 0xe3, + 0x3c, 0x0a, 0xb2, 0xb2, 0xd5, 0xe9, 0x4c, 0xce, 0x3e, 0xcf, 0x79, 0x94, 0x05, 0x59, 0x51, 0xb6, + 0x7a, 0x89, 0xfd, 0x07, 0xda, 0x7c, 0x52, 0x77, 0x9e, 0x24, 0x28, 0xf4, 0xd8, 0x50, 0x40, 0x5e, + 0x12, 0x5f, 0xa1, 0x9a, 0xf8, 0x75, 0xa6, 0x80, 0xff, 0x09, 0x38, 0x83, 0x10, 0x45, 0xc6, 0xf2, + 0x10, 0xb7, 0x6d, 0xe2, 0x8f, 0x46, 0x27, 0xcf, 0x4a, 0x0b, 0xe8, 0xbc, 0x1a, 0x11, 0xf5, 0x1b, + 0x23, 0xe2, 0x09, 0x4f, 0xf8, 0xd1, 0x50, 0xd6, 0x79, 0x9d, 0x69, 0xe4, 0x7f, 0x69, 0x41, 0x83, + 0x66, 0x91, 0xa1, 0xba, 0xf1, 0xba, 0x39, 0x76, 0x2a, 0xe2, 0xcb, 0x60, 0x8a, 0xa2, 0x74, 0xae, + 0xc4, 0x32, 0xe8, 0x93, 0x05, 0x56, 0x0b, 0x5f, 0x23, 0xaa, 0x35, 0x7a, 0xf6, 0x97, 0xbd, 0x64, + 0xd4, 0x1a, 0x91, 0x99, 0x62, 0xd2, 0x83, 0x6c, 0x94, 0x27, 0x28, 0x06, 0xd3, 0x65, 0x10, 0xc9, + 0xa4, 0xb7, 0x99, 0x41, 0xf1, 0x1f, 0xa9, 0x1f, 0x89, 0x8d, 0x89, 0x66, 0x6d, 0xff, 0xe9, 0xb8, + 0x69, 0xb9, 0xff, 0x95, 0x05, 0xad, 0xa7, 0xfa, 0x95, 0x65, 0x7a, 0x61, 0xbd, 0xd2, 0x8b, 0xda, + 0x9a, 0x17, 0x7d, 0xd8, 0x2b, 0x65, 0xd6, 0xee, 0x57, 0x51, 0xd8, 0xca, 0xd3, 0x11, 0x6d, 0x54, + 0xc9, 0x7a, 0x93, 0xbf, 0x8c, 0xb3, 0x75, 0x99, 0x6d, 0x09, 0xdf, 0xc8, 0x4a, 0x17, 0x3a, 0xfa, + 0xef, 0x50, 0xfe, 0x6b, 0xe9, 0xa1, 0x6a, 0x90, 0xfc, 0x3e, 0x34, 0x0f, 0xe3, 0x68, 0x16, 0xcc, + 0xdd, 0x1e, 0x34, 0x06, 0x79, 0xb6, 0x90, 0x1a, 0x3b, 0xfd, 0x3d, 0xa3, 0xf1, 0xf3, 0x6c, 0xa1, + 0x64, 0x98, 0x94, 0xf0, 0xdf, 0x05, 0x58, 0xd1, 0x68, 0x4b, 0xac, 0xb2, 0xf1, 0x0c, 0xaf, 0xa8, + 0x64, 0x52, 0xa9, 0xa5, 0xcd, 0xb6, 0x70, 0xfc, 0xf7, 0xc0, 0x39, 0xc8, 0x83, 0x70, 0x7a, 0x14, + 0xcd, 0x62, 0x1a, 0x1d, 0x17, 0x28, 0xd2, 0x55, 0xbe, 0x4a, 0x48, 0xe1, 0xa6, 0x29, 0x52, 0xf5, + 0x90, 0x46, 0xe3, 0xa6, 0xfc, 0x3b, 0x7f, 0xf0, 0x4b, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x64, + 0x6b, 0x1b, 0xaf, 0x0f, 0x00, 0x00, } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index f5fdef691d..50bd7d5779 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -37,6 +37,20 @@ message DashboardCell { map axes = 9; // Axes represent the graphical viewport for a cell's visualizations repeated Color colors = 10; // Colors represent encoding data values to color Legend legend = 11; // Legend is summary information for a cell + TableOptions tableOptions = 12; // TableOptions for visualization of cell with type 'table' +} + +message TableOptions { + string timeFormat = 1; // format for time + bool verticalTimeAxis = 2; // time axis should be a column not row + TableColumn sortBy = 3; // which column should a table be sorted by + string wrapping = 4; // option for text wrapping + repeated TableColumn columnNames = 5; // names and renames for columns +} + +message TableColumn { + string internalName = 1; // name of column + string displayName = 2; // what column is renamed to } message Color { @@ -95,6 +109,7 @@ message Server { int64 SrcID = 6; // SrcID is the ID of the data source bool Active = 7; // is this the currently active server for the source string Organization = 8; // Organization is the organization ID that resource belongs to + bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client } message Layout { diff --git a/bolt/internal/internal_test.go b/bolt/internal/internal_test.go index b7ca2c0f37..b8397d5d4e 100644 --- a/bolt/internal/internal_test.go +++ b/bolt/internal/internal_test.go @@ -76,12 +76,13 @@ func TestMarshalSourceWithSecret(t *testing.T) { func TestMarshalServer(t *testing.T) { v := chronograf.Server{ - ID: 12, - SrcID: 2, - Name: "Fountain of Truth", - Username: "docbrown", - Password: "1 point twenty-one g1g@w@tts", - URL: "http://oldmanpeabody.mall.io:9092", + ID: 12, + SrcID: 2, + Name: "Fountain of Truth", + Username: "docbrown", + Password: "1 point twenty-one g1g@w@tts", + URL: "http://oldmanpeabody.mall.io:9092", + InsecureSkipVerify: true, } var vv chronograf.Server @@ -193,6 +194,10 @@ func Test_MarshalDashboard(t *testing.T) { Value: "100", }, }, + TableOptions: chronograf.TableOptions{ + TimeFormat: "", + ColumnNames: []chronograf.TableColumn{}, + }, }, }, Templates: []chronograf.Template{}, @@ -255,6 +260,9 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { Type: "static", Orientation: "bottom", }, + TableOptions: chronograf.TableOptions{ + TimeFormat: "MM:DD:YYYY", + }, Type: "line", }, }, @@ -309,6 +317,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { Type: "static", Orientation: "bottom", }, + TableOptions: chronograf.TableOptions{ + TimeFormat: "MM:DD:YYYY", + ColumnNames: []chronograf.TableColumn{}, + }, Type: "line", }, }, @@ -369,6 +381,9 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) { }, }, Type: "line", + TableOptions: chronograf.TableOptions{ + TimeFormat: "MM:DD:YYYY", + }, }, }, Templates: []chronograf.Template{}, @@ -418,6 +433,10 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) { Value: "100", }, }, + TableOptions: chronograf.TableOptions{ + TimeFormat: "MM:DD:YYYY", + ColumnNames: []chronograf.TableColumn{}, + }, Type: "line", }, }, @@ -454,6 +473,9 @@ func Test_MarshalDashboard_WithEmptyCellType(t *testing.T) { Queries: []chronograf.DashboardQuery{}, Axes: map[string]chronograf.Axis{}, CellColors: []chronograf.CellColor{}, + TableOptions: chronograf.TableOptions{ + ColumnNames: []chronograf.TableColumn{}, + }, }, }, Templates: []chronograf.Template{}, diff --git a/bolt/servers_test.go b/bolt/servers_test.go index 7f8b8e201e..a32b6531d9 100644 --- a/bolt/servers_test.go +++ b/bolt/servers_test.go @@ -20,22 +20,24 @@ func TestServerStore(t *testing.T) { srcs := []chronograf.Server{ chronograf.Server{ - Name: "Of Truth", - SrcID: 10, - Username: "marty", - Password: "I❤️ jennifer parker", - URL: "toyota-hilux.lyon-estates.local", - Active: false, - Organization: "133", + Name: "Of Truth", + SrcID: 10, + Username: "marty", + Password: "I❤️ jennifer parker", + URL: "toyota-hilux.lyon-estates.local", + Active: false, + Organization: "133", + InsecureSkipVerify: true, }, chronograf.Server{ - Name: "HipToBeSquare", - SrcID: 12, - Username: "calvinklein", - Password: "chuck b3rry", - URL: "toyota-hilux.lyon-estates.local", - Active: false, - Organization: "133", + Name: "HipToBeSquare", + SrcID: 12, + Username: "calvinklein", + Password: "chuck b3rry", + URL: "toyota-hilux.lyon-estates.local", + Active: false, + Organization: "133", + InsecureSkipVerify: false, }, } diff --git a/bolt/users.go b/bolt/users.go index 2e52834b8d..d0658f8977 100644 --- a/bolt/users.go +++ b/bolt/users.go @@ -150,26 +150,26 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U } // Delete a user from the UsersStore -func (s *UsersStore) Delete(ctx context.Context, usr *chronograf.User) error { - _, err := s.get(ctx, usr.ID) +func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error { + _, err := s.get(ctx, u.ID) if err != nil { return err } return s.client.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(UsersBucket).Delete(u64tob(usr.ID)) + return tx.Bucket(UsersBucket).Delete(u64tob(u.ID)) }) } // Update a user -func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error { - _, err := s.get(ctx, usr.ID) +func (s *UsersStore) Update(ctx context.Context, u *chronograf.User) error { + _, err := s.get(ctx, u.ID) if err != nil { return err } return s.client.db.Update(func(tx *bolt.Tx) error { - if v, err := internal.MarshalUser(usr); err != nil { + if v, err := internal.MarshalUser(u); err != nil { return err - } else if err := tx.Bucket(UsersBucket).Put(u64tob(usr.ID), v); err != nil { + } else if err := tx.Bucket(UsersBucket).Put(u64tob(u.ID), v); err != nil { return err } return nil diff --git a/canned/docker.json b/canned/docker.json index ff0f7b5d39..a27528d3f6 100644 --- a/canned/docker.json +++ b/canned/docker.json @@ -56,6 +56,7 @@ ] } ], + "colors": [], "type": "single-stat" }, { @@ -73,6 +74,7 @@ ] } ], + "colors": [], "type": "single-stat" }, { diff --git a/canned/mesos.json b/canned/mesos.json index 370f67f0c8..beb8645852 100644 --- a/canned/mesos.json +++ b/canned/mesos.json @@ -117,6 +117,7 @@ "h": 4, "i": "0fa47984-825b-46f1-9ca5-0366e3220008", "name": "Mesos Master Uptime", + "colors": [], "type": "single-stat", "queries": [ { diff --git a/chronograf.go b/chronograf.go index 21c98dffc1..5823ad22a9 100644 --- a/chronograf.go +++ b/chronograf.go @@ -35,6 +35,9 @@ const ( ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization") ErrConfigNotFound = Error("cannot find configuration") ErrAnnotationNotFound = Error("annotation not found") + ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'") + ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'") + ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'") ) // Error is a domain error encountered while processing chronograf requests @@ -543,17 +546,33 @@ type Legend struct { // DashboardCell holds visual and query information for a cell type DashboardCell struct { - ID string `json:"i"` - X int32 `json:"x"` - Y int32 `json:"y"` - W int32 `json:"w"` - H int32 `json:"h"` - Name string `json:"name"` - Queries []DashboardQuery `json:"queries"` - Axes map[string]Axis `json:"axes"` - Type string `json:"type"` - CellColors []CellColor `json:"colors"` - Legend Legend `json:"legend"` + ID string `json:"i"` + X int32 `json:"x"` + Y int32 `json:"y"` + W int32 `json:"w"` + H int32 `json:"h"` + Name string `json:"name"` + Queries []DashboardQuery `json:"queries"` + Axes map[string]Axis `json:"axes"` + Type string `json:"type"` + CellColors []CellColor `json:"colors"` + Legend Legend `json:"legend"` + TableOptions TableOptions `json:"tableOptions,omitempty"` +} + +// TableColumn is a column in a DashboardCell of type Table +type TableColumn struct { + InternalName string `json:"internalName"` + DisplayName string `json:"displayName"` +} + +// TableOptions is a type of options for a DashboardCell with type Table +type TableOptions struct { + TimeFormat string `json:"timeFormat"` + VerticalTimeAxis bool `json:"verticalTimeAxis"` + SortBy TableColumn `json:"sortBy"` + Wrapping string `json:"wrapping"` + ColumnNames []TableColumn `json:"columnNames"` } // DashboardsStore is the storage and retrieval of dashboards @@ -572,15 +591,16 @@ 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"` - Axes map[string]Axis `json:"axes"` - 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"` + CellColors []CellColor `json:"colors"` } // Layout is a collection of Cells for visualization diff --git a/integrations/server_test.go b/integrations/server_test.go index cce8a8a516..18f4269e82 100644 --- a/integrations/server_test.go +++ b/integrations/server_test.go @@ -164,7 +164,8 @@ func TestServer(t *testing.T) { "id": "5000", "name": "Kapa 1", "url": "http://localhost:9092", - "active": true, + "active": true, + "insecureSkipVerify": false, "links": { "proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy", "self": "/chronograf/v1/sources/5000/kapacitors/5000", @@ -222,7 +223,8 @@ func TestServer(t *testing.T) { "id": "5000", "name": "Kapa 1", "url": "http://localhost:9092", - "active": true, + "active": true, + "insecureSkipVerify": false, "links": { "proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy", "self": "/chronograf/v1/sources/5000/kapacitors/5000", @@ -540,7 +542,16 @@ func TestServer(t *testing.T) { "legend":{ "type": "static", "orientation": "bottom" - }, + }, + "tableOptions":{ + "timeFormat": "", + "verticalTimeAxis": false, + "sortBy":{ + "internalName": "", + "displayName": ""}, + "wrapping": "", + "columnNames": null + }, "links": { "self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093" } @@ -779,7 +790,17 @@ func TestServer(t *testing.T) { "name": "comet", "value": "100" } - ], + ], + "tableOptions":{ + "timeFormat":"", + "verticalTimeAxis":false, + "sortBy":{ + "internalName":"", + "displayName":"" + }, + "wrapping":"", + "columnNames":null + }, "legend":{ "type": "static", "orientation": "bottom" diff --git a/server/cells_test.go b/server/cells_test.go index 1a22765aae..7e33dea831 100644 --- a/server/cells_test.go +++ b/server/cells_test.go @@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) { } } `))), - want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}} + want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"tableOptions":{"timeFormat":"","verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":""},"wrapping":"","columnNames":null},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}} `, }, { diff --git a/server/kapacitors.go b/server/kapacitors.go index ca6b1d1717..90952d232e 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -16,7 +16,7 @@ type postKapacitorRequest struct { URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true Username string `json:"username,omitempty"` // Username for authentication to kapacitor Password string `json:"password,omitempty"` - InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. Active bool `json:"active"` Organization string `json:"organization"` // Organization is the organization ID that resource belongs to } @@ -55,7 +55,7 @@ type kapacitor struct { URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092) Username string `json:"username,omitempty"` // Username for authentication to kapacitor Password string `json:"password,omitempty"` - InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. Active bool `json:"active"` Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor } @@ -225,7 +225,7 @@ type patchKapacitorRequest struct { URL *string `json:"url,omitempty"` // URL for the kapacitor Username *string `json:"username,omitempty"` // Username for kapacitor auth Password *string `json:"password,omitempty"` - InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + InsecureSkipVerify *bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. Active *bool `json:"active"` } diff --git a/server/layout.go b/server/layout.go index d3870a1363..e2892731c3 100644 --- a/server/layout.go +++ b/server/layout.go @@ -30,6 +30,10 @@ func newLayoutResponse(layout chronograf.Layout) layoutResponse { layout.Cells[idx].Axes = make(map[string]chronograf.Axis, len(axes)) } + if cell.CellColors == nil { + layout.Cells[idx].CellColors = []chronograf.CellColor{} + } + for _, axis := range axes { if _, found := cell.Axes[axis]; !found { layout.Cells[idx].Axes[axis] = chronograf.Axis{ diff --git a/server/layout_test.go b/server/layout_test.go index 6a9479f159..9ff1dbbabe 100644 --- a/server/layout_test.go +++ b/server/layout_test.go @@ -76,12 +76,13 @@ func Test_Layouts(t *testing.T) { Measurement: "influxdb", Cells: []chronograf.Cell{ { - X: 0, - Y: 0, - W: 4, - H: 4, - I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", - Name: "A Graph", + X: 0, + Y: 0, + W: 4, + H: 4, + I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", + Name: "A Graph", + CellColors: []chronograf.CellColor{}, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{}, @@ -103,12 +104,13 @@ func Test_Layouts(t *testing.T) { Measurement: "influxdb", Cells: []chronograf.Cell{ { - X: 0, - Y: 0, - W: 4, - H: 4, - I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", - Name: "A Graph", + X: 0, + Y: 0, + W: 4, + H: 4, + I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", + CellColors: []chronograf.CellColor{}, + Name: "A Graph", }, }, }, diff --git a/ui/mocks/dummy.ts b/ui/mocks/dummy.ts index ffab516810..92bc904933 100644 --- a/ui/mocks/dummy.ts +++ b/ui/mocks/dummy.ts @@ -1,7 +1,51 @@ +export const source = { + id: '2', + name: 'minikube-influx', + type: 'influx', + url: 'http://192.168.99.100:30400', + default: true, + telegraf: 'telegraf', + organization: 'default', + role: 'viewer', + links: { + self: '/chronograf/v1/sources/2', + kapacitors: '/chronograf/v1/sources/2/kapacitors', + proxy: '/chronograf/v1/sources/2/proxy', + queries: '/chronograf/v1/sources/2/queries', + write: '/chronograf/v1/sources/2/write', + permissions: '/chronograf/v1/sources/2/permissions', + users: '/chronograf/v1/sources/2/users', + databases: '/chronograf/v1/sources/2/dbs', + annotations: '/chronograf/v1/sources/2/annotations', + }, +} + export const kapacitor = { id: '1', name: 'Test Kapacitor', url: 'http://localhost:9092', + insecureSkipVerify: false, + active: true, + links: { + self: '/chronograf/v1/sources/47/kapacitors/1', + proxy: '/chronograf/v1/sources/47/kapacitors/1/proxy', + }, +} + +export const createKapacitorBody = { + name: 'Test Kapacitor', + url: 'http://localhost:9092', + insecureSkipVerify: false, + username: 'user', + password: 'pass', +} + +export const updateKapacitorBody = { + name: 'Test Kapacitor', + url: 'http://localhost:9092', + insecureSkipVerify: false, + username: 'user', + password: 'pass', active: true, links: { self: '/chronograf/v1/sources/47/kapacitors/1', diff --git a/ui/mocks/shared/apis/index.js b/ui/mocks/shared/apis/index.ts similarity index 100% rename from ui/mocks/shared/apis/index.js rename to ui/mocks/shared/apis/index.ts diff --git a/ui/mocks/utils/ajax.ts b/ui/mocks/utils/ajax.ts new file mode 100644 index 0000000000..9798925d1e --- /dev/null +++ b/ui/mocks/utils/ajax.ts @@ -0,0 +1 @@ +export default jest.fn(() => Promise.resolve()) diff --git a/ui/package.json b/ui/package.json index dbcd295592..4c53ffdb0d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -136,7 +136,6 @@ "redux-auth-wrapper": "^1.0.0", "redux-thunk": "^1.0.3", "rome": "^2.1.22", - "updeep": "^0.13.0", "uuid": "^3.2.1" } } diff --git a/ui/src/App.js b/ui/src/App.js index e74c929a6a..43c609431a 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,43 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' -import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' import SideNav from 'src/side_nav' import Notifications from 'shared/components/Notifications' -import {publishNotification} from 'shared/actions/notifications' +const App = ({children}) => +
+ + + {children} +
-const {func, node} = PropTypes +const {node} = PropTypes -const App = React.createClass({ - propTypes: { - children: node.isRequired, - notify: func.isRequired, - }, +App.propTypes = { + children: node.isRequired, +} - handleAddFlashMessage({type, text}) { - const {notify} = this.props - - notify(type, text) - }, - - render() { - return ( -
- - - {this.props.children && - React.cloneElement(this.props.children, { - addFlashMessage: this.handleAddFlashMessage, - })} -
- ) - }, -}) - -const mapDispatchToProps = dispatch => ({ - notify: bindActionCreators(publishNotification, dispatch), -}) - -export default connect(null, mapDispatchToProps)(App) +export default App diff --git a/ui/src/CheckSources.js b/ui/src/CheckSources.js index ae5c70f796..b18103253a 100644 --- a/ui/src/CheckSources.js +++ b/ui/src/CheckSources.js @@ -15,9 +15,17 @@ import {showDatabases} from 'shared/apis/metaQuery' import {getSourcesAsync} from 'shared/actions/sources' import {errorThrown as errorThrownAction} from 'shared/actions/errors' -import {publishNotification} from 'shared/actions/notifications' +import {notify as notifyAction} from 'shared/actions/notifications' import {DEFAULT_HOME_PAGE} from 'shared/constants' +import { + NOTIFY_SOURCE_NO_LONGER_AVAILABLE, + NOTIFY_NO_SOURCES_AVAILABLE, + NOTIFY_UNABLE_TO_RETRIEVE_SOURCES, + NOTIFY_USER_REMOVED_FROM_ALL_ORGS, + NOTIFY_USER_REMOVED_FROM_CURRENT_ORG, + NOTIFY_ORG_HAS_NO_SOURCES, +} from 'shared/copy/notifications' // Acts as a 'router middleware'. The main `App` component is responsible for // getting the list of data nodes, but not every page requires them to function. @@ -85,10 +93,7 @@ class CheckSources extends Component { } if (!isFetching && isUsingAuth && !organizations.length) { - notify( - 'error', - 'You have been removed from all organizations. Please contact your administrator.' - ) + notify(NOTIFY_USER_REMOVED_FROM_ALL_ORGS) return router.push('/purgatory') } @@ -96,7 +101,7 @@ class CheckSources extends Component { me.superAdmin && !organizations.find(o => o.id === currentOrganization.id) ) { - notify('error', 'You were removed from your current organization') + notify(NOTIFY_USER_REMOVED_FROM_CURRENT_ORG) return router.push('/purgatory') } @@ -118,7 +123,7 @@ class CheckSources extends Component { return router.push(`/sources/${sources[0].id}/${restString}`) } // if you're a viewer and there are no sources, go to purgatory. - notify('error', 'Organization has no sources configured') + notify(NOTIFY_ORG_HAS_NO_SOURCES) return router.push('/purgatory') } @@ -143,18 +148,12 @@ class CheckSources extends Component { try { const newSources = await getSources() if (newSources.length) { - errorThrown( - error, - `Source ${source.name} is no longer available. Successfully connected to another source.` - ) + errorThrown(error, NOTIFY_SOURCE_NO_LONGER_AVAILABLE(source.name)) } else { - errorThrown( - error, - `Unable to connect to source ${source.name}. No other sources available.` - ) + errorThrown(error, NOTIFY_NO_SOURCES_AVAILABLE(source.name)) } } catch (error2) { - errorThrown(error2, 'Unable to retrieve sources') + errorThrown(error2, NOTIFY_UNABLE_TO_RETRIEVE_SOURCES) } } } @@ -248,7 +247,7 @@ const mapStateToProps = ({sources, auth}) => ({ const mapDispatchToProps = dispatch => ({ getSources: bindActionCreators(getSourcesAsync, dispatch), errorThrown: bindActionCreators(errorThrownAction, dispatch), - notify: bindActionCreators(publishNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)( diff --git a/ui/src/admin/actions/chronograf.js b/ui/src/admin/actions/chronograf.js index 6b8c31d33b..4e155fe1f6 100644 --- a/ui/src/admin/actions/chronograf.js +++ b/ui/src/admin/actions/chronograf.js @@ -16,8 +16,14 @@ import { deleteMapping as deleteMappingAJAX, } from 'src/admin/apis/chronograf' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify} from 'shared/actions/notifications' import {errorThrown} from 'shared/actions/errors' +import { + NOTIFY_MAPPING_DELETED, + NOTIFY_CHRONOGRAF_ORG_DELETED, + NOTIFY_CHRONOGRAF_USER_UPDATED, + NOTIFY_CHRONOGRAF_USER_DELETED, +} from 'shared/copy/notifications' import {REVERT_STATE_DELAY} from 'shared/constants' @@ -177,12 +183,7 @@ export const deleteMappingAsync = mapping => async dispatch => { dispatch(removeMapping(mapping)) try { await deleteMappingAJAX(mapping) - dispatch( - publishAutoDismissingNotification( - 'success', - `Mapping deleted: ${mapping.id} ${mapping.scheme}` - ) - ) + dispatch(notify(NOTIFY_MAPPING_DELETED(mapping.id, mapping.scheme))) } catch (error) { dispatch(errorThrown(error)) dispatch(addMapping(mapping)) @@ -238,7 +239,7 @@ export const updateUserAsync = ( provider: null, scheme: null, }) - dispatch(publishAutoDismissingNotification('success', successMessage)) + dispatch(notify(NOTIFY_CHRONOGRAF_USER_UPDATED(successMessage))) // it's not necessary to syncUser again but it's useful for good // measure and for the clarity of insight in the redux story dispatch(syncUser(user, data)) @@ -256,12 +257,7 @@ export const deleteUserAsync = ( try { await deleteUserAJAX(user) dispatch( - publishAutoDismissingNotification( - 'success', - `${user.name} has been removed from ${isAbsoluteDelete - ? 'all organizations and deleted' - : 'the current organization'}` - ) + notify(NOTIFY_CHRONOGRAF_USER_DELETED(user.name, isAbsoluteDelete)) ) } catch (error) { dispatch(errorThrown(error)) @@ -313,12 +309,7 @@ export const deleteOrganizationAsync = organization => async dispatch => { dispatch(removeOrganization(organization)) try { await deleteOrganizationAJAX(organization) - dispatch( - publishAutoDismissingNotification( - 'success', - `Organization deleted: ${organization.name}` - ) - ) + dispatch(notify(NOTIFY_CHRONOGRAF_ORG_DELETED(organization.name))) } catch (error) { dispatch(errorThrown(error)) dispatch(addOrganization(organization)) diff --git a/ui/src/admin/actions/influxdb.js b/ui/src/admin/actions/influxdb.js index 091124e2d3..530f3507a2 100644 --- a/ui/src/admin/actions/influxdb.js +++ b/ui/src/admin/actions/influxdb.js @@ -18,9 +18,40 @@ import { import {killQuery as killQueryProxy} from 'shared/apis/metaQuery' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify} from 'shared/actions/notifications' import {errorThrown} from 'shared/actions/errors' +import { + NOTIFY_DB_USER_CREATED, + NOTIFY_DB_USER_CREATION_FAILED, + NOTIFY_DB_USER_DELETED, + NOTIFY_DB_USER_DELETION_FAILED, + NOTIFY_DB_USER_PERMISSIONS_UPDATED, + NOTIFY_DB_USER_PERMISSIONS_UPDATE_FAILED, + NOTIFY_DB_USER_ROLES_UPDATED, + NOTIFY_DB_USER_ROLES_UPDATE_FAILED, + NOTIFY_DB_USER_PASSWORD_UPDATED, + NOTIFY_DB_USER_PASSWORD_UPDATE_FAILED, + NOTIFY_DATABASE_CREATED, + NOTIFY_DATABASE_CREATION_FAILED, + NOTIFY_DATABASE_DELETED, + NOTIFY_DATABASE_DELETION_FAILED, + NOTIFY_ROLE_CREATED, + NOTIFY_ROLE_CREATION_FAILED, + NOTIFY_ROLE_DELETED, + NOTIFY_ROLE_DELETION_FAILED, + NOTIFY_ROLE_USERS_UPDATED, + NOTIFY_ROLE_USERS_UPDATE_FAILED, + NOTIFY_ROLE_PERMISSIONS_UPDATED, + NOTIFY_ROLE_PERMISSIONS_UPDATE_FAILED, + NOTIFY_RETENTION_POLICY_CREATED, + NOTIFY_RETENTION_POLICY_CREATION_FAILED, + NOTIFY_RETENTION_POLICY_DELETED, + NOTIFY_RETENTION_POLICY_DELETION_FAILED, + NOTIFY_RETENTION_POLICY_UPDATED, + NOTIFY_RETENTION_POLICY_UPDATE_FAILED, +} from 'shared/copy/notifications' + import {REVERT_STATE_DELAY} from 'shared/constants' import _ from 'lodash' @@ -276,12 +307,12 @@ export const loadDBsAndRPsAsync = url => async dispatch => { export const createUserAsync = (url, user) => async dispatch => { try { const {data} = await createUserAJAX(url, user) - dispatch( - publishAutoDismissingNotification('success', 'User created successfully') - ) + dispatch(notify(NOTIFY_DB_USER_CREATED)) dispatch(syncUser(user, data)) } catch (error) { - dispatch(errorThrown(error, `Failed to create user: ${error.data.message}`)) + dispatch( + errorThrown(error, NOTIFY_DB_USER_CREATION_FAILED(error.data.message)) + ) // undo optimistic update setTimeout(() => dispatch(deleteUser(user)), REVERT_STATE_DELAY) } @@ -290,12 +321,12 @@ export const createUserAsync = (url, user) => async dispatch => { export const createRoleAsync = (url, role) => async dispatch => { try { const {data} = await createRoleAJAX(url, role) - dispatch( - publishAutoDismissingNotification('success', 'Role created successfully') - ) + dispatch(notify(NOTIFY_ROLE_CREATED)) dispatch(syncRole(role, data)) } catch (error) { - dispatch(errorThrown(error, `Failed to create role: ${error.data.message}`)) + dispatch( + errorThrown(error, NOTIFY_ROLE_CREATION_FAILED(error.data.message)) + ) // undo optimistic update setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY) } @@ -305,15 +336,10 @@ export const createDatabaseAsync = (url, database) => async dispatch => { try { const {data} = await createDatabaseAJAX(url, database) dispatch(syncDatabase(database, data)) - dispatch( - publishAutoDismissingNotification( - 'success', - 'Database created successfully' - ) - ) + dispatch(notify(NOTIFY_DATABASE_CREATED)) } catch (error) { dispatch( - errorThrown(error, `Failed to create database: ${error.data.message}`) + errorThrown(error, NOTIFY_DATABASE_CREATION_FAILED(error.data.message)) ) // undo optimistic update setTimeout(() => dispatch(removeDatabase(database)), REVERT_STATE_DELAY) @@ -329,19 +355,11 @@ export const createRetentionPolicyAsync = ( database.links.retentionPolicies, retentionPolicy ) - dispatch( - publishAutoDismissingNotification( - 'success', - 'Retention policy created successfully' - ) - ) + dispatch(notify(NOTIFY_RETENTION_POLICY_CREATED)) dispatch(syncRetentionPolicy(database, retentionPolicy, data)) } catch (error) { dispatch( - errorThrown( - error, - `Failed to create retention policy: ${error.data.message}` - ) + errorThrown(NOTIFY_RETENTION_POLICY_CREATION_FAILED(error.data.message)) ) // undo optimistic update setTimeout( @@ -360,18 +378,13 @@ export const updateRetentionPolicyAsync = ( dispatch(editRetentionPolicyRequested(database, oldRP, newRP)) const {data} = await updateRetentionPolicyAJAX(oldRP.links.self, newRP) dispatch(editRetentionPolicyCompleted(database, oldRP, data)) - dispatch( - publishAutoDismissingNotification( - 'success', - 'Retention policy updated successfully' - ) - ) + dispatch(notify(NOTIFY_RETENTION_POLICY_UPDATED)) } catch (error) { dispatch(editRetentionPolicyFailed(database, oldRP)) dispatch( errorThrown( error, - `Failed to update retention policy: ${error.data.message}` + NOTIFY_RETENTION_POLICY_UPDATE_FAILED(error.data.message) ) ) } @@ -394,9 +407,11 @@ export const deleteRoleAsync = role => async dispatch => { dispatch(deleteRole(role)) try { await deleteRoleAJAX(role.links.self) - dispatch(publishAutoDismissingNotification('success', 'Role deleted')) + dispatch(notify(NOTIFY_ROLE_DELETED(role.name))) } catch (error) { - dispatch(errorThrown(error, `Failed to delete role: ${error.data.message}`)) + dispatch( + errorThrown(error, NOTIFY_ROLE_DELETION_FAILED(error.data.message)) + ) } } @@ -404,9 +419,11 @@ export const deleteUserAsync = user => async dispatch => { dispatch(deleteUser(user)) try { await deleteUserAJAX(user.links.self) - dispatch(publishAutoDismissingNotification('success', 'User deleted')) + dispatch(notify(NOTIFY_DB_USER_DELETED(user.name))) } catch (error) { - dispatch(errorThrown(error, `Failed to delete user: ${error.data.message}`)) + dispatch( + errorThrown(error, NOTIFY_DB_USER_DELETION_FAILED(error.data.message)) + ) } } @@ -414,10 +431,10 @@ export const deleteDatabaseAsync = database => async dispatch => { dispatch(removeDatabase(database)) try { await deleteDatabaseAJAX(database.links.self) - dispatch(publishAutoDismissingNotification('success', 'Database deleted')) + dispatch(notify(NOTIFY_DATABASE_DELETED(database.name))) } catch (error) { dispatch( - errorThrown(error, `Failed to delete database: ${error.data.message}`) + errorThrown(error, NOTIFY_DATABASE_DELETION_FAILED(error.data.message)) ) } } @@ -429,17 +446,12 @@ export const deleteRetentionPolicyAsync = ( dispatch(removeRetentionPolicy(database, retentionPolicy)) try { await deleteRetentionPolicyAJAX(retentionPolicy.links.self) - dispatch( - publishAutoDismissingNotification( - 'success', - `Retention policy ${retentionPolicy.name} deleted` - ) - ) + dispatch(notify(NOTIFY_RETENTION_POLICY_DELETED(retentionPolicy.name))) } catch (error) { dispatch( errorThrown( error, - `Failed to delete retentionPolicy: ${error.data.message}` + NOTIFY_RETENTION_POLICY_DELETION_FAILED(error.data.message) ) ) } @@ -452,10 +464,12 @@ export const updateRoleUsersAsync = (role, users) => async dispatch => { users, role.permissions ) - dispatch(publishAutoDismissingNotification('success', 'Role users updated')) + dispatch(notify(NOTIFY_ROLE_USERS_UPDATED)) dispatch(syncRole(role, data)) } catch (error) { - dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`)) + dispatch( + errorThrown(error, NOTIFY_ROLE_USERS_UPDATE_FAILED(error.data.message)) + ) } } @@ -469,13 +483,14 @@ export const updateRolePermissionsAsync = ( role.users, permissions ) - dispatch( - publishAutoDismissingNotification('success', 'Role permissions updated') - ) + dispatch(notify(NOTIFY_ROLE_PERMISSIONS_UPDATED)) dispatch(syncRole(role, data)) } catch (error) { dispatch( - errorThrown(error, `Failed to update role: ${error.data.message}`) + errorThrown( + error, + NOTIFY_ROLE_PERMISSIONS_UPDATE_FAILED(error.data.message) + ) ) } } @@ -486,13 +501,14 @@ export const updateUserPermissionsAsync = ( ) => async dispatch => { try { const {data} = await updateUserAJAX(user.links.self, {permissions}) - dispatch( - publishAutoDismissingNotification('success', 'User permissions updated') - ) + dispatch(notify(NOTIFY_DB_USER_PERMISSIONS_UPDATED)) dispatch(syncUser(user, data)) } catch (error) { dispatch( - errorThrown(error, `Failed to update user: ${error.data.message}`) + errorThrown( + error, + NOTIFY_DB_USER_PERMISSIONS_UPDATE_FAILED(error.data.message) + ) ) } } @@ -500,11 +516,11 @@ export const updateUserPermissionsAsync = ( export const updateUserRolesAsync = (user, roles) => async dispatch => { try { const {data} = await updateUserAJAX(user.links.self, {roles}) - dispatch(publishAutoDismissingNotification('success', 'User roles updated')) + dispatch(notify(NOTIFY_DB_USER_ROLES_UPDATED)) dispatch(syncUser(user, data)) } catch (error) { dispatch( - errorThrown(error, `Failed to update user: ${error.data.message}`) + errorThrown(error, NOTIFY_DB_USER_ROLES_UPDATE_FAILED(error.data.message)) ) } } @@ -512,13 +528,14 @@ export const updateUserRolesAsync = (user, roles) => async dispatch => { export const updateUserPasswordAsync = (user, password) => async dispatch => { try { const {data} = await updateUserAJAX(user.links.self, {password}) - dispatch( - publishAutoDismissingNotification('success', 'User password updated') - ) + dispatch(notify(NOTIFY_DB_USER_PASSWORD_UPDATED)) dispatch(syncUser(user, data)) } catch (error) { dispatch( - errorThrown(error, `Failed to update user: ${error.data.message}`) + errorThrown( + error, + NOTIFY_DB_USER_PASSWORD_UPDATE_FAILED(error.data.message) + ) ) } } diff --git a/ui/src/admin/components/DatabaseManager.js b/ui/src/admin/components/DatabaseManager.js index 08f343d159..f8e27b34bd 100644 --- a/ui/src/admin/components/DatabaseManager.js +++ b/ui/src/admin/components/DatabaseManager.js @@ -5,7 +5,6 @@ import DatabaseTable from 'src/admin/components/DatabaseTable' const DatabaseManager = ({ databases, - notify, isRFDisplayed, isAddDBDisabled, addDatabase, @@ -46,7 +45,6 @@ const DatabaseManager = ({ ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(onClickOutside(DatabaseRow)) diff --git a/ui/src/admin/components/DatabaseTable.js b/ui/src/admin/components/DatabaseTable.js index 084d5a9dad..8e97bb5a1d 100644 --- a/ui/src/admin/components/DatabaseTable.js +++ b/ui/src/admin/components/DatabaseTable.js @@ -12,7 +12,6 @@ const {func, shape, bool} = PropTypes const DatabaseTable = ({ database, - notify, isRFDisplayed, onEditDatabase, onKeyDownDatabase, @@ -36,7 +35,6 @@ const DatabaseTable = ({ > { if (database.deleteCode !== `DELETE ${database.name}`) { - return notify('error', `Type DELETE ${database.name} to confirm`) + return notify(NOTIFY_DATABASE_DELETE_CONFIRMATION_REQUIRED(database.name)) } onDelete(db) @@ -136,7 +141,7 @@ const {func, shape, bool} = PropTypes DatabaseTableHeader.propTypes = { onEdit: func, - notify: func, + notify: func.isRequired, database: shape(), onKeyDown: func, onCancel: func, @@ -150,7 +155,7 @@ DatabaseTableHeader.propTypes = { } Header.propTypes = { - notify: func, + notify: func.isRequired, onConfirm: func, onCancel: func, onDelete: func, @@ -170,4 +175,8 @@ EditHeader.propTypes = { isRFDisplayed: bool, } -export default DatabaseTableHeader +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(DatabaseTableHeader) diff --git a/ui/src/admin/components/chronograf/AllUsersTable.js b/ui/src/admin/components/chronograf/AllUsersTable.js index 0fec81bf0a..9ae1c259a6 100644 --- a/ui/src/admin/components/chronograf/AllUsersTable.js +++ b/ui/src/admin/components/chronograf/AllUsersTable.js @@ -16,6 +16,11 @@ const { colActions, } = ALL_USERS_TABLE +import { + NOTIFY_CHRONOGRAF_USER_ADDED_TO_ORG, + NOTIFY_CHRONOGRAF_USER_REMOVED_FROM_ORG, +} from 'shared/copy/notifications' + class AllUsersTable extends Component { constructor(props) { super(props) @@ -47,7 +52,7 @@ class AllUsersTable extends Component { this.props.onUpdateUserRoles( user, newRoles, - `${user.name} has been added to ${organization.name}` + NOTIFY_CHRONOGRAF_USER_ADDED_TO_ORG(user.name, organization.name) ) } @@ -61,7 +66,7 @@ class AllUsersTable extends Component { this.props.onUpdateUserRoles( user, newRoles, - `${user.name} has been removed from ${name}` + NOTIFY_CHRONOGRAF_USER_REMOVED_FROM_ORG(user.name, name) ) } @@ -84,7 +89,6 @@ class AllUsersTable extends Component { onCreateUser, authConfig, meID, - notify, onDeleteUser, isLoading, } = this.props @@ -154,7 +158,6 @@ class AllUsersTable extends Component { organizations={organizations} onBlur={this.handleBlurCreateUserRow} onCreateUser={onCreateUser} - notify={notify} /> : null} @@ -209,7 +212,6 @@ AllUsersTable.propTypes = { superAdminNewUsers: bool, }), meID: string.isRequired, - notify: func.isRequired, isLoading: bool.isRequired, } diff --git a/ui/src/admin/components/chronograf/AllUsersTableRowNew.js b/ui/src/admin/components/chronograf/AllUsersTableRowNew.js index 2724c25497..406f5e2907 100644 --- a/ui/src/admin/components/chronograf/AllUsersTableRowNew.js +++ b/ui/src/admin/components/chronograf/AllUsersTableRowNew.js @@ -1,8 +1,12 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' +import {notify as notifyAction} from 'shared/actions/notifications' import Dropdown from 'shared/components/Dropdown' +import {NOTIFY_CHRONOGRAF_USER_MISSING_NAME_AND_PROVIDER} from 'shared/copy/notifications' import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing' const { colOrganizations, @@ -80,8 +84,7 @@ class AllUsersTableRowNew extends Component { if (e.key === 'Enter') { if (preventCreate) { return this.props.notify( - 'warning', - 'User must have a name and provider' + NOTIFY_CHRONOGRAF_USER_MISSING_NAME_AND_PROVIDER ) } this.handleConfirmCreateUser() @@ -181,4 +184,8 @@ AllUsersTableRowNew.propTypes = { notify: func.isRequired, } -export default AllUsersTableRowNew +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(AllUsersTableRowNew) diff --git a/ui/src/admin/components/chronograf/UsersTable.js b/ui/src/admin/components/chronograf/UsersTable.js index 491b8e538d..cea56a139e 100644 --- a/ui/src/admin/components/chronograf/UsersTable.js +++ b/ui/src/admin/components/chronograf/UsersTable.js @@ -35,14 +35,7 @@ class UsersTable extends Component { } render() { - const { - organization, - users, - onCreateUser, - meID, - notify, - isLoading, - } = this.props + const {organization, users, onCreateUser, meID, isLoading} = this.props const {isCreatingUser} = this.state const {colRole, colProvider, colScheme, colActions} = USERS_TABLE @@ -83,7 +76,6 @@ class UsersTable extends Component { organization={organization} onBlur={this.handleBlurCreateUserRow} onCreateUser={onCreateUser} - notify={notify} /> : null} {users.length @@ -138,7 +130,6 @@ UsersTable.propTypes = { onUpdateUserRole: func.isRequired, onDeleteUser: func.isRequired, meID: string.isRequired, - notify: func.isRequired, isLoading: bool.isRequired, } diff --git a/ui/src/admin/components/chronograf/UsersTableRowNew.js b/ui/src/admin/components/chronograf/UsersTableRowNew.js index df72f94010..9b05fac0e5 100644 --- a/ui/src/admin/components/chronograf/UsersTableRowNew.js +++ b/ui/src/admin/components/chronograf/UsersTableRowNew.js @@ -1,8 +1,13 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import {notify as notifyAction} from 'shared/actions/notifications' import Dropdown from 'shared/components/Dropdown' +import {NOTIFY_CHRONOGRAF_USER_MISSING_NAME_AND_PROVIDER} from 'shared/copy/notifications' import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing' import {USER_ROLES} from 'src/admin/constants/chronografAdmin' @@ -61,8 +66,7 @@ class UsersTableRowNew extends Component { if (e.key === 'Enter') { if (preventCreate) { return this.props.notify( - 'warning', - 'User must have a name and provider' + NOTIFY_CHRONOGRAF_USER_MISSING_NAME_AND_PROVIDER ) } this.handleConfirmCreateUser() @@ -148,4 +152,8 @@ UsersTableRowNew.propTypes = { notify: func.isRequired, } -export default UsersTableRowNew +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(UsersTableRowNew) diff --git a/ui/src/admin/containers/AdminInfluxDBPage.js b/ui/src/admin/containers/AdminInfluxDBPage.js index 8269458ef3..76fe7d6a68 100644 --- a/ui/src/admin/containers/AdminInfluxDBPage.js +++ b/ui/src/admin/containers/AdminInfluxDBPage.js @@ -29,7 +29,12 @@ import AdminTabs from 'src/admin/components/AdminTabs' import SourceIndicator from 'shared/components/SourceIndicator' import FancyScrollbar from 'shared/components/FancyScrollbar' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify as notifyAction} from 'shared/actions/notifications' + +import { + NOTIFY_ROLE_NAME_INVALID, + NOTIFY_DB_USER_NAME_PASSWORD_INVALID, +} from 'shared/copy/notifications' const isValidUser = user => { const minLen = 3 @@ -75,7 +80,7 @@ class AdminInfluxDBPage extends Component { handleSaveUser = async user => { const {notify} = this.props if (!isValidUser(user)) { - notify('error', 'Username and/or password too short') + notify(NOTIFY_DB_USER_NAME_PASSWORD_INVALID) return } if (user.isNew) { @@ -88,7 +93,7 @@ class AdminInfluxDBPage extends Component { handleSaveRole = async role => { const {notify} = this.props if (!isValidRole(role)) { - notify('error', 'Role name too short') + notify(NOTIFY_ROLE_NAME_INVALID) return } if (role.isNew) { @@ -229,7 +234,7 @@ AdminInfluxDBPage.propTypes = { updateUserPermissions: func, updateUserRoles: func, updateUserPassword: func, - notify: func, + notify: func.isRequired, } const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({ @@ -265,7 +270,7 @@ const mapDispatchToProps = dispatch => ({ ), updateUserRoles: bindActionCreators(updateUserRolesAsync, dispatch), updateUserPassword: bindActionCreators(updateUserPasswordAsync, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(AdminInfluxDBPage) diff --git a/ui/src/admin/containers/DatabaseManagerPage.js b/ui/src/admin/containers/DatabaseManagerPage.js index dc04a15968..2448ea4173 100644 --- a/ui/src/admin/containers/DatabaseManagerPage.js +++ b/ui/src/admin/containers/DatabaseManagerPage.js @@ -7,7 +7,13 @@ import _ from 'lodash' import DatabaseManager from 'src/admin/components/DatabaseManager' import * as adminActionCreators from 'src/admin/actions/influxdb' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify as notifyAction} from 'shared/actions/notifications' + +import { + NOTIFY_DATABASE_DELETE_CONFIRMATION_REQUIRED, + NOTIFY_DATABASE_NAME_ALREADY_EXISTS, + NOTIFY_DATABASE_NAME_INVALID, +} from 'shared/copy/notifications' class DatabaseManagerPage extends Component { constructor(props) { @@ -35,11 +41,11 @@ class DatabaseManagerPage extends Component { handleCreateDatabase = database => { const {actions, notify, source, databases} = this.props if (!database.name) { - return notify('error', 'Database name cannot be blank') + return notify(NOTIFY_DATABASE_NAME_INVALID) } if (_.findIndex(databases, {name: database.name}, 1) !== -1) { - return notify('error', 'A database by this name already exists') + return notify(NOTIFY_DATABASE_NAME_ALREADY_EXISTS) } actions.createDatabaseAsync(source.links.databases, database) @@ -60,11 +66,11 @@ class DatabaseManagerPage extends Component { if (key === 'Enter') { if (!database.name) { - return notify('error', 'Database name cannot be blank') + return notify(NOTIFY_DATABASE_NAME_INVALID) } if (_.findIndex(databases, {name: database.name}, 1) !== -1) { - return notify('error', 'A database by this name already exists') + return notify(NOTIFY_DATABASE_NAME_ALREADY_EXISTS) } actions.createDatabaseAsync(source.links.databases, database) @@ -81,7 +87,9 @@ class DatabaseManagerPage extends Component { if (key === 'Enter') { if (database.deleteCode !== `DELETE ${database.name}`) { - return notify('error', `Please type DELETE ${database.name} to confirm`) + return notify( + NOTIFY_DATABASE_DELETE_CONFIRMATION_REQUIRED(database.name) + ) } return actions.deleteDatabaseAsync(database) @@ -153,7 +161,7 @@ DatabaseManagerPage.propTypes = { removeRetentionPolicy: func, deleteRetentionPolicyAsync: func, }), - notify: func, + notify: func.isRequired, } const mapStateToProps = ({adminInfluxDB: {databases, retentionPolicies}}) => ({ @@ -163,7 +171,7 @@ const mapStateToProps = ({adminInfluxDB: {databases, retentionPolicies}}) => ({ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(adminActionCreators, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(DatabaseManagerPage) diff --git a/ui/src/admin/containers/ProvidersPage.js b/ui/src/admin/containers/ProvidersPage.js index 44ba591afe..64bcef0de7 100644 --- a/ui/src/admin/containers/ProvidersPage.js +++ b/ui/src/admin/containers/ProvidersPage.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import * as adminChronografActionCreators from 'src/admin/actions/chronograf' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify as notifyAction} from 'shared/actions/notifications' import ProvidersTable from 'src/admin/components/chronograf/ProvidersTable' @@ -96,7 +96,7 @@ const mapStateToProps = ({ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(adminChronografActionCreators, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(ProvidersPage) diff --git a/ui/src/admin/containers/QueriesPage.js b/ui/src/admin/containers/QueriesPage.js index 5624678e17..709e6e7d7b 100644 --- a/ui/src/admin/containers/QueriesPage.js +++ b/ui/src/admin/containers/QueriesPage.js @@ -12,13 +12,15 @@ import QueriesTable from 'src/admin/components/QueriesTable' import showDatabasesParser from 'shared/parsing/showDatabases' import showQueriesParser from 'shared/parsing/showQueries' import {TIMES} from 'src/admin/constants' +import {NOTIFY_QUERIES_ERROR} from 'shared/copy/notifications' + import { loadQueries as loadQueriesAction, setQueryToKill as setQueryToKillAction, killQueryAsync, } from 'src/admin/actions/influxdb' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify as notifyAction} from 'shared/actions/notifications' class QueriesPage extends Component { componentDidMount() { @@ -42,7 +44,7 @@ class QueriesPage extends Component { showDatabases(source.links.proxy).then(resp => { const {databases, errors} = showDatabasesParser(resp.data) if (errors.length) { - errors.forEach(message => notify('error', message)) + errors.forEach(message => notify(NOTIFY_QUERIES_ERROR(message))) return } @@ -53,7 +55,9 @@ class QueriesPage extends Component { queryResponses.forEach(queryResponse => { const result = showQueriesParser(queryResponse.data) if (result.errors.length) { - result.errors.forEach(message => notify('error', message)) + result.errors.forEach(message => + notify(NOTIFY_QUERIES_ERROR(message)) + ) } allQueries.push(...result.queries) @@ -92,7 +96,7 @@ QueriesPage.propTypes = { queryIDToKill: string, setQueryToKill: func, killQuery: func, - notify: func, + notify: func.isRequired, } const mapStateToProps = ({adminInfluxDB: {queries, queryIDToKill}}) => ({ @@ -104,7 +108,7 @@ const mapDispatchToProps = dispatch => ({ loadQueries: bindActionCreators(loadQueriesAction, dispatch), setQueryToKill: bindActionCreators(setQueryToKillAction, dispatch), killQuery: bindActionCreators(killQueryAsync, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(QueriesPage) diff --git a/ui/src/admin/containers/chronograf/AllUsersPage.js b/ui/src/admin/containers/chronograf/AllUsersPage.tsx similarity index 66% rename from ui/src/admin/containers/chronograf/AllUsersPage.js rename to ui/src/admin/containers/chronograf/AllUsersPage.tsx index 57e7515fa8..6027908286 100644 --- a/ui/src/admin/containers/chronograf/AllUsersPage.js +++ b/ui/src/admin/containers/chronograf/AllUsersPage.tsx @@ -1,15 +1,44 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import * as adminChronografActionCreators from 'src/admin/actions/chronograf' -import * as configActionCreators from 'shared/actions/config' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import * as configActionCreators from 'src/shared/actions/config' +import {notify as notifyAction} from 'src/shared/actions/notifications' import AllUsersTable from 'src/admin/components/chronograf/AllUsersTable' +import {AuthLinks, User, Role, Organization} from 'src/types' -class AllUsersPage extends Component { +interface Props { + notify: () => void + links: AuthLinks + meID: string + users: User[] + organizations: Organization[] + actionsAdmin: { + loadUsersAsync: (link: string) => void + loadOrganizationsAsync: (link: string) => void + createUserAsync: (link: string, user: User) => void + updateUserAsync: (user: User, updatedUser: User, message: string) => void + deleteUserAsync: ( + user: User, + deleteObj: {isAbsoluteDelete: boolean} + ) => void + } + actionsConfig: { + getAuthConfigAsync: (link: string) => void + updateAuthConfigAsync: () => void + } + authConfig: { + superAdminNewUsers: boolean + } +} + +interface State { + isLoading: boolean +} + +export class AllUsersPage extends PureComponent { constructor(props) { super(props) @@ -23,32 +52,6 @@ class AllUsersPage extends Component { getAuthConfigAsync(links.config.auth) } - handleCreateUser = user => { - const {links, actionsAdmin: {createUserAsync}} = this.props - createUserAsync(links.allUsers, user) - } - - handleUpdateUserRoles = (user, roles, successMessage) => { - const {actionsAdmin: {updateUserAsync}} = this.props - const updatedUser = {...user, roles} - updateUserAsync(user, updatedUser, successMessage) - } - - handleUpdateUserSuperAdmin = (user, superAdmin) => { - const {actionsAdmin: {updateUserAsync}} = this.props - const updatedUser = {...user, superAdmin} - updateUserAsync( - user, - updatedUser, - `${user.name}'s SuperAdmin status has been updated` - ) - } - - handleDeleteUser = user => { - const {actionsAdmin: {deleteUserAsync}} = this.props - deleteUserAsync(user, {isAbsoluteDelete: true}) - } - async componentWillMount() { const { links, @@ -65,65 +68,66 @@ class AllUsersPage extends Component { this.setState({isLoading: false}) } + handleCreateUser = (user: User) => { + const {links, actionsAdmin: {createUserAsync}} = this.props + createUserAsync(links.allUsers, user) + } + + handleUpdateUserRoles = ( + user: User, + roles: Role[], + successMessage: string + ) => { + const {actionsAdmin: {updateUserAsync}} = this.props + const updatedUser = {...user, roles} + updateUserAsync(user, updatedUser, successMessage) + } + + handleUpdateUserSuperAdmin = (user: User, superAdmin: boolean) => { + const {actionsAdmin: {updateUserAsync}} = this.props + const updatedUser = {...user, superAdmin} + updateUserAsync( + user, + updatedUser, + `${user.name}'s SuperAdmin status has been updated` + ) + } + + handleDeleteUser = (user: User) => { + const {actionsAdmin: {deleteUserAsync}} = this.props + deleteUserAsync(user, {isAbsoluteDelete: true}) + } + render() { const { - organizations, meID, users, - authConfig, - actionsConfig, links, notify, + authConfig, + actionsConfig, + organizations, } = this.props return ( ) } } -const {arrayOf, bool, func, shape, string} = PropTypes - -AllUsersPage.propTypes = { - links: shape({ - users: string.isRequired, - config: shape({ - auth: string.isRequired, - }).isRequired, - }), - meID: string.isRequired, - users: arrayOf(shape), - organizations: arrayOf(shape), - actionsAdmin: shape({ - loadUsersAsync: func.isRequired, - loadOrganizationsAsync: func.isRequired, - createUserAsync: func.isRequired, - updateUserAsync: func.isRequired, - deleteUserAsync: func.isRequired, - }), - actionsConfig: shape({ - getAuthConfigAsync: func.isRequired, - updateAuthConfigAsync: func.isRequired, - }), - authConfig: shape({ - superAdminNewUsers: bool, - }), - notify: func.isRequired, -} - const mapStateToProps = ({ links, adminChronograf: {organizations, users}, @@ -138,7 +142,7 @@ const mapStateToProps = ({ const mapDispatchToProps = dispatch => ({ actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch), actionsConfig: bindActionCreators(configActionCreators, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(AllUsersPage) diff --git a/ui/src/admin/containers/chronograf/UsersPage.js b/ui/src/admin/containers/chronograf/UsersPage.js index 4c433bcbd6..0d79e31b66 100644 --- a/ui/src/admin/containers/chronograf/UsersPage.js +++ b/ui/src/admin/containers/chronograf/UsersPage.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import * as adminChronografActionCreators from 'src/admin/actions/chronograf' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify as notifyAction} from 'shared/actions/notifications' import UsersTable from 'src/admin/components/chronograf/UsersTable' @@ -116,7 +116,7 @@ const mapStateToProps = ({links, adminChronograf: {organizations, users}}) => ({ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(adminChronografActionCreators, dispatch), - notify: bindActionCreators(publishAutoDismissingNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(UsersPage) diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index c70e4f2612..34b4a5354e 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -23,17 +23,17 @@ export const renameCell = cellName => ({ }, }) -export const updateSingleStatColors = singleStatColors => ({ - type: 'UPDATE_SINGLE_STAT_COLORS', +export const updateThresholdsListColors = thresholdsListColors => ({ + type: 'UPDATE_THRESHOLDS_LIST_COLORS', payload: { - singleStatColors, + thresholdsListColors, }, }) -export const updateSingleStatType = singleStatType => ({ - type: 'UPDATE_SINGLE_STAT_TYPE', +export const updateThresholdsListType = thresholdsListType => ({ + type: 'UPDATE_THRESHOLDS_LIST_TYPE', payload: { - singleStatType, + thresholdsListType, }, }) @@ -50,3 +50,10 @@ export const updateAxes = axes => ({ axes, }, }) + +export const updateTableOptions = tableOptions => ({ + type: 'UPDATE_TABLE_OPTIONS', + payload: { + tableOptions, + }, +}) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 8400d638bc..2cafbd89b8 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -8,10 +8,14 @@ import { runTemplateVariableQuery, } from 'src/dashboards/apis' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify} from 'shared/actions/notifications' import {errorThrown} from 'shared/actions/errors' import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants' +import { + NOTIFY_DASHBOARD_DELETED, + NOTIFY_DASHBOARD_DELETE_FAILED, +} from 'shared/copy/notifications' import { TEMPLATE_VARIABLE_SELECTED, @@ -257,15 +261,13 @@ export const deleteDashboardAsync = dashboard => async dispatch => { dispatch(deleteDashboard(dashboard)) try { await deleteDashboardAJAX(dashboard) - dispatch( - publishAutoDismissingNotification( - 'success', - 'Dashboard deleted successfully.' - ) - ) + dispatch(notify(NOTIFY_DASHBOARD_DELETED(dashboard.name))) } catch (error) { dispatch( - errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`) + errorThrown( + error, + NOTIFY_DASHBOARD_DELETE_FAILED(dashboard.name, error.data.message) + ) ) dispatch(deleteDashboardFailed(dashboard)) } diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index cbee8faef6..4f48a7e037 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -8,12 +8,15 @@ import Input from 'src/dashboards/components/DisplayOptionsInput' import {Tabber, Tab} from 'src/dashboards/components/Tabber' import FancyScrollbar from 'shared/components/FancyScrollbar' -import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants' +import { + AXES_SCALE_OPTIONS, + TOOLTIP_Y_VALUE_FORMAT, +} from 'src/dashboards/constants/cellEditor' import {GRAPH_TYPES} from 'src/dashboards/graphics/graph' import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay' -const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS +const {LINEAR, LOG, BASE_2, BASE_10} = AXES_SCALE_OPTIONS const getInputMin = scale => (scale === LOG ? '0' : null) class AxesOptions extends Component { @@ -140,7 +143,7 @@ class AxesOptions extends Component { { const {queriesWorkingDraft, staticLegend} = this.state - const {cell, singleStatColors, gaugeColors} = this.props + const {cell, thresholdsListColors, gaugeColors} = this.props const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} @@ -120,13 +120,18 @@ class CellEditorOverlay extends Component { }) let colors = [] - if (cell.type === 'gauge') { - colors = stringifyColorValues(gaugeColors) - } else if ( - cell.type === 'single-stat' || - cell.type === 'line-plus-single-stat' - ) { - colors = stringifyColorValues(singleStatColors) + + switch (cell.type) { + case 'gauge': { + colors = stringifyColorValues(gaugeColors) + break + } + case 'single-stat': + case 'line-plus-single-stat': + case 'table': { + colors = stringifyColorValues(thresholdsListColors) + break + } } this.props.onSave({ @@ -375,8 +380,8 @@ CellEditorOverlay.propTypes = { }).isRequired, dashboardID: string.isRequired, sources: arrayOf(shape()), - singleStatType: string.isRequired, - singleStatColors: arrayOf(shape({}).isRequired).isRequired, + thresholdsListType: string.isRequired, + thresholdsListColors: arrayOf(shape({}).isRequired).isRequired, gaugeColors: arrayOf(shape({}).isRequired).isRequired, } diff --git a/ui/src/dashboards/components/DisplayOptions.js b/ui/src/dashboards/components/DisplayOptions.js index 80204eb104..8c08f21eca 100644 --- a/ui/src/dashboards/components/DisplayOptions.js +++ b/ui/src/dashboards/components/DisplayOptions.js @@ -42,6 +42,7 @@ class DisplayOptions extends Component { staticLegend, onToggleStaticLegend, onResetFocus, + queryConfigs, } = this.props switch (type) { case 'gauge': @@ -49,7 +50,12 @@ class DisplayOptions extends Component { case 'single-stat': return case 'table': - return + return ( + + ) default: return ( color.value) if (sortedColors.length <= MAX_THRESHOLDS) { - const randomColor = _.random(0, GAUGE_COLORS.length - 1) + const randomColor = _.random(0, THRESHOLD_COLORS.length - 1) const maxValue = sortedColors[sortedColors.length - 1].value const minValue = sortedColors[0].value @@ -43,8 +43,8 @@ class GaugeOptions extends Component { type: COLOR_TYPE_THRESHOLD, id: uuid.v4(), value: randomValue, - hex: GAUGE_COLORS[randomColor].hex, - name: GAUGE_COLORS[randomColor].name, + hex: THRESHOLD_COLORS[randomColor].hex, + name: THRESHOLD_COLORS[randomColor].name, } const updatedColors = _.sortBy( @@ -165,9 +165,9 @@ class GaugeOptions extends Component { >
Gauge Controls
-
+
- {sortedColors.map( - color => - color.id === SINGLE_STAT_BASE - ?
-
Base Color
- -
- : - )} -
- ) -} -const {arrayOf, bool, func, shape, string, number} = PropTypes - -GraphOptionsThresholds.propTypes = { - onAddThreshold: func, - disableAddThreshold: bool, - sortedColors: arrayOf( - shape({ - hex: string, - id: string, - name: string, - type: string, - value: number, - }) - ), - formatColor: func, - onChooseColor: func, - onValidateColorValue: func, - onUpdateColorValue: func, - onDeleteThreshold: func, -} - -export default GraphOptionsThresholds diff --git a/ui/src/dashboards/components/GraphOptionsTimeFormat.js b/ui/src/dashboards/components/GraphOptionsTimeFormat.js deleted file mode 100644 index f1680d8193..0000000000 --- a/ui/src/dashboards/components/GraphOptionsTimeFormat.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -const GraphOptionsTimeFormat = ({TimeFormat, onTimeFormatChange}) => -
- - -
- -const {func, string} = PropTypes - -GraphOptionsTimeFormat.propTypes = { - TimeFormat: string, - onTimeFormatChange: func, -} - -export default GraphOptionsTimeFormat diff --git a/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx b/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx new file mode 100644 index 0000000000..b20cbec48f --- /dev/null +++ b/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx @@ -0,0 +1,90 @@ +import React, {PureComponent} from 'react' +import InputClickToEdit from 'src/shared/components/InputClickToEdit' +import {Dropdown} from 'src/shared/components/Dropdown' +import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip' +import { + FORMAT_OPTIONS, + TIME_FORMAT_DEFAULT, + TIME_FORMAT_CUSTOM, +} from 'src/shared/constants/tableGraph' + +interface TimeFormatOptions { + text: string +} + +interface Props { + timeFormat: string + onTimeFormatChange: (format: string) => void +} + +interface State { + customFormat: boolean + format: string +} + +class GraphOptionsTimeFormat extends PureComponent { + constructor(props: Props) { + super(props) + this.state = { + format: this.props.timeFormat || TIME_FORMAT_DEFAULT, + customFormat: false, + } + } + + get onTimeFormatChange() { + return this.props.onTimeFormatChange + } + + handleChooseFormat = (formatOption: TimeFormatOptions) => { + if (formatOption.text === TIME_FORMAT_CUSTOM) { + this.setState({customFormat: true}) + } else { + this.setState({format: formatOption.text, customFormat: false}) + this.onTimeFormatChange(formatOption.text) + } + } + + render() { + const {format, customFormat} = this.state + const {onTimeFormatChange} = this.props + const tipContent = + 'For information on formatting, see http://momentjs.com/docs/#/parsing/string-format/' + + const formatOption = FORMAT_OPTIONS.find(op => op.text === format) + const showCustom = !formatOption || customFormat + + return ( +
+ + + {showCustom && +
+ +
} +
+ ) + } +} + +export default GraphOptionsTimeFormat diff --git a/ui/src/dashboards/components/SingleStatOptions.js b/ui/src/dashboards/components/SingleStatOptions.js index 50d559042d..5909d9fd51 100644 --- a/ui/src/dashboards/components/SingleStatOptions.js +++ b/ui/src/dashboards/components/SingleStatOptions.js @@ -3,121 +3,13 @@ import PropTypes from 'prop-types' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' -import _ from 'lodash' -import uuid from 'uuid' - import FancyScrollbar from 'shared/components/FancyScrollbar' -import Threshold from 'src/dashboards/components/Threshold' -import ColorDropdown from 'shared/components/ColorDropdown' +import ThresholdsList from 'shared/components/ThresholdsList' +import ThresholdsListTypeToggle from 'shared/components/ThresholdsListTypeToggle' -import { - GAUGE_COLORS, - DEFAULT_VALUE_MIN, - DEFAULT_VALUE_MAX, - MAX_THRESHOLDS, - SINGLE_STAT_BASE, - SINGLE_STAT_TEXT, - SINGLE_STAT_BG, -} from 'src/dashboards/constants/gaugeColors' - -import { - updateSingleStatType, - updateSingleStatColors, - updateAxes, -} from 'src/dashboards/actions/cellEditorOverlay' - -const formatColor = color => { - const {hex, name} = color - return {hex, name} -} +import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay' class SingleStatOptions extends Component { - handleToggleSingleStatType = newType => () => { - const {handleUpdateSingleStatType} = this.props - - handleUpdateSingleStatType(newType) - } - - handleAddThreshold = () => { - const { - singleStatColors, - singleStatType, - handleUpdateSingleStatColors, - onResetFocus, - } = this.props - - const randomColor = _.random(0, GAUGE_COLORS.length - 1) - - const maxValue = DEFAULT_VALUE_MIN - const minValue = DEFAULT_VALUE_MAX - - let randomValue = _.round(_.random(minValue, maxValue, true), 2) - - if (singleStatColors.length > 0) { - const colorsValues = _.mapValues(singleStatColors, 'value') - do { - randomValue = _.round(_.random(minValue, maxValue, true), 2) - } while (_.includes(colorsValues, randomValue)) - } - - const newThreshold = { - type: singleStatType, - id: uuid.v4(), - value: randomValue, - hex: GAUGE_COLORS[randomColor].hex, - name: GAUGE_COLORS[randomColor].name, - } - - const updatedColors = _.sortBy( - [...singleStatColors, newThreshold], - color => color.value - ) - - handleUpdateSingleStatColors(updatedColors) - onResetFocus() - } - - handleDeleteThreshold = threshold => () => { - const {handleUpdateSingleStatColors, onResetFocus} = this.props - const singleStatColors = this.props.singleStatColors.filter( - color => color.id !== threshold.id - ) - const sortedColors = _.sortBy(singleStatColors, color => color.value) - - handleUpdateSingleStatColors(sortedColors) - onResetFocus() - } - - handleChooseColor = threshold => chosenColor => { - const {handleUpdateSingleStatColors} = this.props - - const singleStatColors = this.props.singleStatColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - handleUpdateSingleStatColors(singleStatColors) - } - - handleUpdateColorValue = (threshold, value) => { - const {handleUpdateSingleStatColors} = this.props - - const singleStatColors = this.props.singleStatColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - handleUpdateSingleStatColors(singleStatColors) - } - - handleValidateColorValue = (threshold, targetValue) => { - const {singleStatColors} = this.props - const sortedColors = _.sortBy(singleStatColors, color => color.value) - - return !sortedColors.some(color => color.value === targetValue) - } - handleUpdatePrefix = e => { const {handleUpdateAxes, axes} = this.props const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}} @@ -132,21 +24,8 @@ class SingleStatOptions extends Component { handleUpdateAxes(newAxes) } - handleSortColors = () => { - const {singleStatColors, handleUpdateSingleStatColors} = this.props - const sortedColors = _.sortBy(singleStatColors, color => color.value) - - handleUpdateSingleStatColors(sortedColors) - } - render() { - const { - singleStatColors, - singleStatType, - axes: {y: {prefix, suffix}}, - } = this.props - - const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS + const {axes: {y: {prefix, suffix}}, onResetFocus} = this.props return (
Single Stat Controls
-
- - {singleStatColors.map( - color => - color.id === SINGLE_STAT_BASE - ?
-
Base Color
- -
- : - )} -
+
@@ -208,27 +56,7 @@ class SingleStatOptions extends Component { maxLength="5" />
-
- -
    -
  • - Background -
  • -
  • - Text -
  • -
-
+
@@ -236,47 +64,19 @@ class SingleStatOptions extends Component { } } -const {arrayOf, func, number, shape, string} = PropTypes - -SingleStatOptions.defaultProps = { - colors: [], -} +const {func, shape} = PropTypes SingleStatOptions.propTypes = { - singleStatType: string.isRequired, - singleStatColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), - handleUpdateSingleStatType: func.isRequired, - handleUpdateSingleStatColors: func.isRequired, handleUpdateAxes: func.isRequired, axes: shape({}).isRequired, onResetFocus: func.isRequired, } -const mapStateToProps = ({ - cellEditorOverlay: {singleStatType, singleStatColors, cell: {axes}}, -}) => ({ - singleStatType, - singleStatColors, +const mapStateToProps = ({cellEditorOverlay: {cell: {axes}}}) => ({ axes, }) const mapDispatchToProps = dispatch => ({ - handleUpdateSingleStatType: bindActionCreators( - updateSingleStatType, - dispatch - ), - handleUpdateSingleStatColors: bindActionCreators( - updateSingleStatColors, - dispatch - ), handleUpdateAxes: bindActionCreators(updateAxes, dispatch), }) diff --git a/ui/src/dashboards/components/TableOptions.js b/ui/src/dashboards/components/TableOptions.js deleted file mode 100644 index 5113fc3309..0000000000 --- a/ui/src/dashboards/components/TableOptions.js +++ /dev/null @@ -1,177 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' - -import _ from 'lodash' - -import FancyScrollbar from 'shared/components/FancyScrollbar' -import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' -import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis' -import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy' -import GraphOptionsTextWrapping from 'src/dashboards/components/GraphOptionsTextWrapping' -import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns' -import GraphOptionsThresholds from 'src/dashboards/components/GraphOptionsThresholds' -import GraphOptionsThresholdColoring from 'src/dashboards/components/GraphOptionsThresholdColoring' - -import {MAX_THRESHOLDS} from 'src/dashboards/constants/gaugeColors' - -import { - updateSingleStatType, - updateSingleStatColors, - updateAxes, -} from 'src/dashboards/actions/cellEditorOverlay' - -const formatColor = color => { - const {hex, name} = color - return {hex, name} -} - -class TableOptions extends Component { - state = {TimeAxis: 'VERTICAL', TimeFormat: 'mm/dd/yyyy HH:mm:ss.ss'} - - handleToggleSingleStatType = () => {} - - handleAddThreshold = () => {} - - handleDeleteThreshold = () => () => {} - - handleChooseColor = () => () => {} - - handleChooseSortBy = () => {} - - handleTimeFormatChange = () => {} - - handleToggleTimeAxis = () => {} - - handleToggleTextWrapping = () => {} - - handleColumnRename = () => {} - - handleUpdateColorValue = () => {} - - handleValidateColorValue = () => {} - - render() { - const { - singleStatColors, - singleStatType, - // axes: {y: {prefix, suffix}}, - } = this.props - - const {TimeFormat, TimeAxis} = this.state - - const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS - - const sortedColors = _.sortBy(singleStatColors, color => color.value) - - const columns = [ - 'cpu.mean_usage_system', - 'cpu.mean_usage_idle', - 'cpu.mean_usage_user', - ].map(col => ({ - text: col, - name: col, - newName: '', - })) - const tableSortByOptions = [ - 'cpu.mean_usage_system', - 'cpu.mean_usage_idle', - 'cpu.mean_usage_user', - ].map(col => ({text: col})) - - return ( - -
-
Table Controls
-
- - - - -
- - -
- -
-
-
- ) - } -} - -const {arrayOf, func, number, shape, string} = PropTypes - -TableOptions.defaultProps = { - colors: [], -} - -TableOptions.propTypes = { - singleStatType: string.isRequired, - singleStatColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), - handleUpdateSingleStatType: func.isRequired, - handleUpdateSingleStatColors: func.isRequired, - handleUpdateAxes: func.isRequired, - axes: shape({}).isRequired, -} - -const mapStateToProps = ({ - cellEditorOverlay: {singleStatType, singleStatColors, cell: {axes}}, -}) => ({ - singleStatType, - singleStatColors, - axes, -}) - -const mapDispatchToProps = dispatch => ({ - handleUpdateSingleStatType: bindActionCreators( - updateSingleStatType, - dispatch - ), - handleUpdateSingleStatColors: bindActionCreators( - updateSingleStatColors, - dispatch - ), - handleUpdateAxes: bindActionCreators(updateAxes, dispatch), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(TableOptions) diff --git a/ui/src/dashboards/components/TableOptions.tsx b/ui/src/dashboards/components/TableOptions.tsx new file mode 100644 index 0000000000..1d83eb0edc --- /dev/null +++ b/ui/src/dashboards/components/TableOptions.tsx @@ -0,0 +1,161 @@ +import React, {PureComponent} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import _ from 'lodash' + +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' +import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis' +import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy' +import GraphOptionsTextWrapping from 'src/dashboards/components/GraphOptionsTextWrapping' +import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns' +import ThresholdsList from 'src/shared/components/ThresholdsList' +import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle' + +import {TIME_COLUMN_DEFAULT} from 'src/shared/constants/tableGraph' +import {updateTableOptions} from 'src/dashboards/actions/cellEditorOverlay' + +type TableColumn = { + internalName: string + displayName: string +} + +type Options = { + timeFormat: string + verticalTimeAxis: boolean + sortBy: TableColumn + wrapping: string + columnNames: TableColumn[] +} + +type QueryConfig = { + measurement: string + fields: [ + { + alias: string + value: string + } + ] +} + +interface Props { + queryConfigs: QueryConfig[] + handleUpdateTableOptions: (options: Options) => void + tableOptions: Options + onResetFocus: () => void +} + +export class TableOptions extends PureComponent { + constructor(props) { + super(props) + } + + componentWillMount() { + const {queryConfigs, handleUpdateTableOptions, tableOptions} = this.props + const {columnNames} = tableOptions + const timeColumn = + (columnNames && columnNames.find(c => c.internalName === 'time')) || + TIME_COLUMN_DEFAULT + + const columns = [ + timeColumn, + ..._.flatten( + queryConfigs.map(qc => { + const {measurement, fields} = qc + return fields.map(f => { + const internalName = `${measurement}.${f.alias}` + const existing = columnNames.find( + c => c.internalName === internalName + ) + return existing || {internalName, displayName: ''} + }) + }) + ), + ] + + handleUpdateTableOptions({...tableOptions, columnNames: columns}) + } + + handleChooseSortBy = () => {} + + handleTimeFormatChange = timeFormat => { + const {tableOptions, handleUpdateTableOptions} = this.props + handleUpdateTableOptions({...tableOptions, timeFormat}) + } + + handleToggleTimeAxis = () => {} + + handleToggleTextWrapping = () => {} + + handleColumnRename = column => { + const {handleUpdateTableOptions, tableOptions} = this.props + const {columnNames} = tableOptions + const updatedColumns = columnNames.map( + op => (op.internalName === column.internalName ? column : op) + ) + handleUpdateTableOptions({...tableOptions, columnNames: updatedColumns}) + } + + render() { + const { + tableOptions: {timeFormat, columnNames: columns}, + onResetFocus, + } = this.props + + const TimeAxis = 'vertical' + + const tableSortByOptions = [ + 'cpu.mean_usage_system', + 'cpu.mean_usage_idle', + 'cpu.mean_usage_user', + ].map(col => ({text: col})) + + return ( + +
+
Table Controls
+
+ + + + +
+ + +
+ +
+
+
+ ) + } +} + +const mapStateToProps = ({cellEditorOverlay: {cell: {tableOptions}}}) => ({ + tableOptions, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateTableOptions: bindActionCreators(updateTableOptions, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(TableOptions) diff --git a/ui/src/dashboards/components/Threshold.js b/ui/src/dashboards/components/Threshold.js index bf572e9890..8f34dbf1c6 100644 --- a/ui/src/dashboards/components/Threshold.js +++ b/ui/src/dashboards/components/Threshold.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import ColorDropdown from 'shared/components/ColorDropdown' -import {GAUGE_COLORS} from 'src/dashboards/constants/gaugeColors' +import {THRESHOLD_COLORS} from 'shared/constants/thresholds' class Threshold extends Component { constructor(props) { @@ -54,14 +54,14 @@ class Threshold extends Component { const selectedColor = {hex, name} let label = 'Threshold' - let labelClass = 'gauge-controls--label-editable' + let labelClass = 'threshold-item--label__editable' let canBeDeleted = true if (visualizationType === 'gauge') { labelClass = isMin || isMax - ? 'gauge-controls--label' - : 'gauge-controls--label-editable' + ? 'threshold-item--label' + : 'threshold-item--label__editable' canBeDeleted = !(isMin || isMax) } @@ -73,17 +73,17 @@ class Threshold extends Component { } const inputClass = valid - ? 'form-control input-sm gauge-controls--input' - : 'form-control input-sm gauge-controls--input form-volcano' + ? 'form-control input-sm threshold-item--input' + : 'form-control input-sm threshold-item--input form-volcano' return ( -
+
{label}
{canBeDeleted ?
- { - - } +
diff --git a/ui/src/kapacitor/components/KapacitorRule.js b/ui/src/kapacitor/components/KapacitorRule.js index f7fde915f9..ca60f66c0b 100644 --- a/ui/src/kapacitor/components/KapacitorRule.js +++ b/ui/src/kapacitor/components/KapacitorRule.js @@ -1,5 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import NameSection from 'src/kapacitor/components/NameSection' import ValuesSection from 'src/kapacitor/components/ValuesSection' @@ -12,6 +14,17 @@ import {createRule, editRule} from 'src/kapacitor/apis' import buildInfluxQLQuery from 'utils/influxql' import {timeRanges} from 'shared/data/timeRanges' import {DEFAULT_RULE_ID} from 'src/kapacitor/constants' +import {notify as notifyAction} from 'shared/actions/notifications' + +import { + NOTIFY_ALERT_RULE_CREATED, + NOTIFY_ALERT_RULE_CREATION_FAILED, + NOTIFY_ALERT_RULE_UPDATED, + NOTIFY_ALERT_RULE_UPDATE_FAILED, + NOTIFY_ALERT_RULE_REQUIRES_QUERY, + NOTIFY_ALERT_RULE_REQUIRES_CONDITION_VALUE, + NOTIFY_ALERT_RULE_DEADMAN_INVALID, +} from 'shared/copy/notifications' class KapacitorRule extends Component { constructor(props) { @@ -27,14 +40,7 @@ class KapacitorRule extends Component { } handleCreate = pathname => { - const { - addFlashMessage, - queryConfigs, - rule, - source, - router, - kapacitor, - } = this.props + const {notify, queryConfigs, rule, source, router, kapacitor} = this.props const newRule = Object.assign({}, rule, { query: queryConfigs[rule.queryID], @@ -44,18 +50,15 @@ class KapacitorRule extends Component { createRule(kapacitor, newRule) .then(() => { router.push(pathname || `/sources/${source.id}/alert-rules`) - addFlashMessage({type: 'success', text: 'Rule successfully created'}) + notify(NOTIFY_ALERT_RULE_CREATED) }) .catch(() => { - addFlashMessage({ - type: 'error', - text: 'There was a problem creating the rule', - }) + notify(NOTIFY_ALERT_RULE_CREATION_FAILED) }) } handleEdit = pathname => { - const {addFlashMessage, queryConfigs, rule, router, source} = this.props + const {notify, queryConfigs, rule, router, source} = this.props const updatedRule = Object.assign({}, rule, { query: queryConfigs[rule.queryID], }) @@ -63,16 +66,10 @@ class KapacitorRule extends Component { editRule(updatedRule) .then(() => { router.push(pathname || `/sources/${source.id}/alert-rules`) - addFlashMessage({ - type: 'success', - text: `${rule.name} successfully saved!`, - }) + notify(NOTIFY_ALERT_RULE_UPDATED(rule.name)) }) .catch(e => { - addFlashMessage({ - type: 'error', - text: `There was a problem saving ${rule.name}: ${e.data.message}`, - }) + notify(NOTIFY_ALERT_RULE_UPDATE_FAILED(rule.name, e.data.message)) }) } @@ -118,11 +115,11 @@ class KapacitorRule extends Component { } if (!buildInfluxQLQuery({}, query)) { - return 'Please select a Database, Measurement, and Field' + return NOTIFY_ALERT_RULE_REQUIRES_QUERY } if (!rule.values.value) { - return 'Please enter a value in the Conditions section' + return NOTIFY_ALERT_RULE_REQUIRES_CONDITION_VALUE } return '' @@ -131,7 +128,7 @@ class KapacitorRule extends Component { deadmanValidation = () => { const {query} = this.props if (query && (!query.database || !query.measurement)) { - return 'Deadman rules require a Database and Measurement' + return NOTIFY_ALERT_RULE_DEADMAN_INVALID } return '' @@ -234,7 +231,7 @@ KapacitorRule.propTypes = { queryConfigs: shape({}).isRequired, queryConfigActions: shape({}).isRequired, ruleActions: shape({}).isRequired, - addFlashMessage: func.isRequired, + notify: func.isRequired, ruleID: string.isRequired, handlersFromConfig: arrayOf(shape({})).isRequired, router: shape({ @@ -244,4 +241,8 @@ KapacitorRule.propTypes = { configLink: string.isRequired, } -export default KapacitorRule +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(KapacitorRule) diff --git a/ui/src/kapacitor/containers/KapacitorPage.tsx b/ui/src/kapacitor/containers/KapacitorPage.tsx index eabfa17f16..1676a71dad 100644 --- a/ui/src/kapacitor/containers/KapacitorPage.tsx +++ b/ui/src/kapacitor/containers/KapacitorPage.tsx @@ -1,5 +1,9 @@ import React, {PureComponent, ChangeEvent} from 'react' import {withRouter} from 'react-router' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import {notify as notifyAction} from 'src/shared/actions/notifications' import {Source} from 'src/types' @@ -12,10 +16,27 @@ import { import KapacitorForm from '../components/KapacitorForm' +import { + NOTIFY_KAPACITOR_CONNECTION_FAILED, + NOTIFY_KAPACITOR_NAME_ALREADY_TAKEN, + NOTIFY_KAPACITOR_UPDATED, + NOTIFY_KAPACITOR_UPDATE_FAILED, + NOTIFY_KAPACITOR_CREATED, + NOTIFY_KAPACITOR_CREATION_FAILED, +} from 'src/shared/copy/notifications' + export const defaultName = 'My Kapacitor' export const kapacitorPort = '9092' -type FlashMessage = {type: string; text: string} +export interface Notification { + id?: string + type: string + icon: string + duration: number + message: string +} + +export type NotificationFunc = () => Notification interface Kapacitor { url: string @@ -23,6 +44,7 @@ interface Kapacitor { username: string password: string active: boolean + insecureSkipVerify: boolean links: { self: string } @@ -30,7 +52,7 @@ interface Kapacitor { interface Props { source: Source - addFlashMessage: (message: FlashMessage) => void + notify: (message: Notification | NotificationFunc) => void kapacitor: Kapacitor router: {push: (url: string) => void} location: {pathname: string; hash: string} @@ -46,24 +68,15 @@ export class KapacitorPage extends PureComponent { constructor(props) { super(props) this.state = { - kapacitor: { - url: this.parseKapacitorURL(), - name: defaultName, - username: '', - password: '', - active: false, - links: { - self: '', - }, - }, - exists: false, + kapacitor: this.defaultKapacitor, + exists: false } this.handleSubmit = this.handleSubmit.bind(this) } async componentDidMount() { - const {source, params: {id}, addFlashMessage} = this.props + const {source, params: {id}, notify} = this.props if (!id) { return } @@ -72,15 +85,23 @@ export class KapacitorPage extends PureComponent { const kapacitor = await getKapacitor(source, id) this.setState({kapacitor}) await this.checkKapacitorConnection(kapacitor) - } catch (err) { - console.error('Could not get kapacitor: ', err) - addFlashMessage({ - type: 'error', - text: 'Could not connect to Kapacitor', - }) + } catch (error) { + console.error('Could not get kapacitor: ', error) + notify(NOTIFY_KAPACITOR_CONNECTION_FAILED) } } + handleCheckboxChange = (e: ChangeEvent) => { + const {checked} = e.target + + this.setState({ + kapacitor: { + ...this.state.kapacitor, + insecureSkipVerify: checked + } + }) + } + handleInputChange = (e: ChangeEvent) => { const {value, name} = e.target @@ -97,7 +118,7 @@ export class KapacitorPage extends PureComponent { handleSubmit = async e => { e.preventDefault() const { - addFlashMessage, + notify, source, source: {kapacitors = []}, params, @@ -110,10 +131,7 @@ export class KapacitorPage extends PureComponent { const isNew = !params.id if (isNew && isNameTaken) { - addFlashMessage({ - type: 'error', - text: `There is already a Kapacitor configuration named "${kapacitor.name}"`, - }) + notify(NOTIFY_KAPACITOR_NAME_ALREADY_TAKEN) return } @@ -122,13 +140,10 @@ export class KapacitorPage extends PureComponent { const {data} = await updateKapacitor(kapacitor) this.setState({kapacitor: data}) this.checkKapacitorConnection(data) - addFlashMessage({type: 'success', text: 'Kapacitor Updated!'}) + notify(NOTIFY_KAPACITOR_UPDATED) } catch (error) { console.error(error) - addFlashMessage({ - type: 'error', - text: 'There was a problem updating the Kapacitor record', - }) + notify(NOTIFY_KAPACITOR_UPDATE_FAILED) } } else { try { @@ -137,34 +152,31 @@ export class KapacitorPage extends PureComponent { this.setState({kapacitor: data}) this.checkKapacitorConnection(data) router.push(`/sources/${source.id}/kapacitors/${data.id}/edit`) - addFlashMessage({ - type: 'success', - text: 'Kapacitor Created! Configuring endpoints is optional.', - }) + notify(NOTIFY_KAPACITOR_CREATED) } catch (error) { console.error(error) - addFlashMessage({ - type: 'error', - text: 'There was a problem creating the Kapacitor record', - }) + notify(NOTIFY_KAPACITOR_CREATION_FAILED) } } } handleResetToDefaults = e => { e.preventDefault() - const defaultState = { + this.setState({kapacitor: {...this.defaultKapacitor}}) + } + + private get defaultKapacitor() { + return { url: this.parseKapacitorURL(), name: defaultName, username: '', password: '', active: false, + insecureSkipVerify: false, links: { self: '', }, } - - this.setState({kapacitor: {...defaultState}}) } private checkKapacitorConnection = async (kapacitor: Kapacitor) => { @@ -172,11 +184,9 @@ export class KapacitorPage extends PureComponent { await pingKapacitor(kapacitor) this.setState({exists: true}) } catch (error) { + console.error(error) this.setState({exists: false}) - this.props.addFlashMessage({ - type: 'error', - text: 'Could not connect to Kapacitor. Check settings.', - }) + this.props.notify(NOTIFY_KAPACITOR_CONNECTION_FAILED) } } @@ -188,7 +198,7 @@ export class KapacitorPage extends PureComponent { } render() { - const {source, addFlashMessage, location, params} = this.props + const {source, location, params, notify} = this.props const hash = (location && location.hash) || (params && params.hash) || '' const {kapacitor, exists} = this.state @@ -199,13 +209,18 @@ export class KapacitorPage extends PureComponent { exists={exists} kapacitor={kapacitor} onSubmit={this.handleSubmit} - addFlashMessage={addFlashMessage} onChangeUrl={this.handleChangeUrl} onReset={this.handleResetToDefaults} onInputChange={this.handleInputChange} + notify={notify} + onCheckboxChange={this.handleCheckboxChange} /> ) } } -export default withRouter(KapacitorPage) +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(withRouter(KapacitorPage)) diff --git a/ui/src/kapacitor/containers/KapacitorRulePage.js b/ui/src/kapacitor/containers/KapacitorRulePage.js index daf9728505..a0c4f283ca 100644 --- a/ui/src/kapacitor/containers/KapacitorRulePage.js +++ b/ui/src/kapacitor/containers/KapacitorRulePage.js @@ -10,6 +10,12 @@ import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index' import {DEFAULT_RULE_ID} from 'src/kapacitor/constants' import KapacitorRule from 'src/kapacitor/components/KapacitorRule' import parseHandlersFromConfig from 'src/shared/parsing/parseHandlersFromConfig' +import {notify as notifyAction} from 'shared/actions/notifications' + +import { + NOTIFY_KAPACITOR_CREATION_FAILED, + NOTIFY_COULD_NOT_FIND_KAPACITOR, +} from 'shared/copy/notifications' class KapacitorRulePage extends Component { constructor(props) { @@ -22,7 +28,7 @@ class KapacitorRulePage extends Component { } async componentDidMount() { - const {params, source, ruleActions, addFlashMessage} = this.props + const {params, source, ruleActions, notify} = this.props if (params.ruleID === 'new') { ruleActions.loadDefaultRule() @@ -32,10 +38,7 @@ class KapacitorRulePage extends Component { const kapacitor = await getActiveKapacitor(this.props.source) if (!kapacitor) { - return addFlashMessage({ - type: 'error', - text: "We couldn't find a configured Kapacitor for this source", // eslint-disable-line quotes - }) + return notify(NOTIFY_COULD_NOT_FIND_KAPACITOR) } try { @@ -43,10 +46,7 @@ class KapacitorRulePage extends Component { const handlersFromConfig = parseHandlersFromConfig(kapacitorConfig) this.setState({kapacitor, handlersFromConfig}) } catch (error) { - addFlashMessage({ - type: 'error', - text: 'There was a problem communicating with Kapacitor', - }) + notify(NOTIFY_KAPACITOR_CREATION_FAILED) console.error(error) throw error } @@ -60,7 +60,6 @@ class KapacitorRulePage extends Component { router, ruleActions, queryConfigs, - addFlashMessage, queryConfigActions, } = this.props const {handlersFromConfig, kapacitor} = this.state @@ -79,7 +78,6 @@ class KapacitorRulePage extends Component { queryConfigs={queryConfigs} queryConfigActions={queryConfigActions} ruleActions={ruleActions} - addFlashMessage={addFlashMessage} handlersFromConfig={handlersFromConfig} ruleID={params.ruleID} router={router} @@ -99,7 +97,7 @@ KapacitorRulePage.propTypes = { self: string.isRequired, }), }), - addFlashMessage: func, + notify: func, rules: shape({}).isRequired, queryConfigs: shape({}).isRequired, ruleActions: shape({ @@ -128,6 +126,7 @@ const mapStateToProps = ({rules, kapacitorQueryConfigs: queryConfigs}) => ({ const mapDispatchToProps = dispatch => ({ ruleActions: bindActionCreators(kapacitorRuleActionCreators, dispatch), + notify: bindActionCreators(notifyAction, dispatch), queryConfigActions: bindActionCreators( kapacitorQueryConfigActionCreators, dispatch diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index 65fa946fad..1f1dd74d2f 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -78,7 +78,6 @@ KapacitorRulesPage.propTypes = { deleteRule: func.isRequired, updateRuleStatus: func.isRequired, }).isRequired, - addFlashMessage: func, } const mapStateToProps = state => { diff --git a/ui/src/kapacitor/containers/KapacitorTasksPage.js b/ui/src/kapacitor/containers/KapacitorTasksPage.js index 5ae7aed4c2..552b465ab1 100644 --- a/ui/src/kapacitor/containers/KapacitorTasksPage.js +++ b/ui/src/kapacitor/containers/KapacitorTasksPage.js @@ -12,7 +12,6 @@ export const KapacitorTasksPage = React.createClass({ kapacitors: PropTypes.string.isRequired, }).isRequired, }).isRequired, - addFlashMessage: PropTypes.func, }, getInitialState() { diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js index 92bad77cca..00721c2ab5 100644 --- a/ui/src/kapacitor/containers/TickscriptPage.js +++ b/ui/src/kapacitor/containers/TickscriptPage.js @@ -9,7 +9,13 @@ import * as kapactiorActionCreators from 'src/kapacitor/actions/view' import * as errorActionCreators from 'shared/actions/errors' import {getActiveKapacitor} from 'src/shared/apis' import {getLogStreamByRuleID, pingKapacitorVersion} from 'src/kapacitor/apis' -import {publishNotification} from 'shared/actions/notifications' +import {notify as notifyAction} from 'shared/actions/notifications' + +import { + NOTIFY_TICKSCRIPT_LOGGING_UNAVAILABLE, + NOTIFY_TICKSCRIPT_LOGGING_ERROR, + NOTIFY_KAPACITOR_NOT_FOUND, +} from 'shared/copy/notifications' class TickscriptPage extends Component { constructor(props) { @@ -44,11 +50,7 @@ class TickscriptPage extends Component { this.setState({ areLogsEnabled: false, }) - notify( - 'warning', - 'Could not use logging, requires Kapacitor version 1.4', - {once: true} - ) + notify(NOTIFY_TICKSCRIPT_LOGGING_UNAVAILABLE) return } @@ -117,7 +119,7 @@ class TickscriptPage extends Component { } } catch (error) { console.error(error) - notify('error', error) + notify(NOTIFY_TICKSCRIPT_LOGGING_ERROR(error)) throw error } } @@ -132,9 +134,7 @@ class TickscriptPage extends Component { const kapacitor = await getActiveKapacitor(source) if (!kapacitor) { - errorActions.errorThrown( - 'We could not find a configured Kapacitor for this source' - ) + errorActions.errorThrown(NOTIFY_KAPACITOR_NOT_FOUND) } if (this._isEditing()) { @@ -287,7 +287,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => ({ kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch), errorActions: bindActionCreators(errorActionCreators, dispatch), - notify: bindActionCreators(publishNotification, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage) diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 55c7aff557..6b8dfc5957 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -9,8 +9,9 @@ export const loadLocalStorage = errorsQueue => { // eslint-disable-next-line no-undef if (state.VERSION && state.VERSION !== VERSION) { - const errorText = - 'New version of Chronograf detected. Local settings cleared.' + // eslint-disable-next-line no-undef + const version = VERSION ? ` (${VERSION})` : '' + const errorText = `Welcome to the latest Chronograf ${version}. Local settings cleared.` console.log(errorText) // eslint-disable-line no-console errorsQueue.push(errorText) @@ -52,7 +53,6 @@ export const saveToLocalStorage = ({ timeRange, dataExplorer, dashTimeV1: {ranges}, - dismissedNotifications, }) => { try { const appPersisted = Object.assign({}, {app: {persisted}}) @@ -67,7 +67,6 @@ export const saveToLocalStorage = ({ dataExplorer, VERSION, // eslint-disable-line no-undef dashTimeV1, - dismissedNotifications, }) ) } catch (err) { diff --git a/ui/src/shared/actions/auth.js b/ui/src/shared/actions/auth.js index d5d2258eb8..8b99280f76 100644 --- a/ui/src/shared/actions/auth.js +++ b/ui/src/shared/actions/auth.js @@ -2,10 +2,10 @@ import {getMe as getMeAJAX, updateMe as updateMeAJAX} from 'shared/apis/auth' import {getLinksAsync} from 'shared/actions/links' -import {publishAutoDismissingNotification} from 'shared/dispatchers' +import {notify} from 'shared/actions/notifications' import {errorThrown} from 'shared/actions/errors' -import {NOTIFICATION_DISMISS_DELAY} from 'shared/constants' +import {NOTIFY_USER_SWITCHED_ORGS} from 'shared/copy/notifications' export const authExpired = auth => ({ type: 'AUTH_EXPIRED', @@ -92,11 +92,8 @@ export const meChangeOrganizationAsync = ( r => r.organization === me.currentOrganization.id ) dispatch( - publishAutoDismissingNotification( - 'success', - `Now logged in to '${me.currentOrganization - .name}' as '${currentRole.name}'`, - NOTIFICATION_DISMISS_DELAY + notify( + NOTIFY_USER_SWITCHED_ORGS(me.currentOrganization.name, currentRole.name) ) ) dispatch(meChangeOrganizationCompleted()) diff --git a/ui/src/shared/actions/notifications.js b/ui/src/shared/actions/notifications.js index 23c830ce6d..5e1fbce2cd 100644 --- a/ui/src/shared/actions/notifications.js +++ b/ui/src/shared/actions/notifications.js @@ -1,31 +1,9 @@ -export function publishNotification(type, message, options = {once: false}) { - // this validator is purely for development purposes. It might make sense to move this to a middleware. - const validTypes = ['error', 'success', 'warning'] - if (!validTypes.includes(type) || message === undefined) { - console.error('handleNotification must have a valid type and text') // eslint-disable-line no-console - } +export const notify = notification => ({ + type: 'PUBLISH_NOTIFICATION', + payload: {notification}, +}) - return { - type: 'NOTIFICATION_RECEIVED', - payload: { - type, - message, - once: options.once, - }, - } -} - -export function dismissNotification(type) { - return { - type: 'NOTIFICATION_DISMISSED', - payload: { - type, - }, - } -} - -export function dismissAllNotifications() { - return { - type: 'ALL_NOTIFICATIONS_DISMISSED', - } -} +export const dismissNotification = id => ({ + type: 'DISMISS_NOTIFICATION', + payload: {id}, +}) diff --git a/ui/src/shared/actions/sources.js b/ui/src/shared/actions/sources.js index e19710492e..f81009baca 100644 --- a/ui/src/shared/actions/sources.js +++ b/ui/src/shared/actions/sources.js @@ -5,10 +5,15 @@ import { updateKapacitor as updateKapacitorAJAX, deleteKapacitor as deleteKapacitorAJAX, } from 'shared/apis' -import {publishNotification} from './notifications' +import {notify} from './notifications' import {errorThrown} from 'shared/actions/errors' import {HTTP_NOT_FOUND} from 'shared/constants' +import { + NOTIFY_SERVER_ERROR, + NOTIFY_COULD_NOT_RETRIEVE_KAPACITORS, + NOTIFY_COULD_NOT_DELETE_KAPACITOR, +} from 'shared/copy/notifications' export const loadSources = sources => ({ type: 'LOAD_SOURCES', @@ -71,9 +76,7 @@ export const removeAndLoadSources = source => async dispatch => { const {data: {sources: newSources}} = await getSourcesAJAX() dispatch(loadSources(newSources)) } catch (err) { - dispatch( - publishNotification('error', 'Internal Server Error. Check API Logs') - ) + dispatch(notify(NOTIFY_SERVER_ERROR)) } } @@ -82,12 +85,7 @@ export const fetchKapacitorsAsync = source => async dispatch => { const {data} = await getKapacitorsAJAX(source) dispatch(fetchKapacitors(source, data.kapacitors)) } catch (err) { - dispatch( - publishNotification( - 'error', - `Internal Server Error. Could not retrieve kapacitors for source ${source.id}.` - ) - ) + dispatch(notify(NOTIFY_COULD_NOT_RETRIEVE_KAPACITORS(source.id))) } } @@ -103,12 +101,7 @@ export const deleteKapacitorAsync = kapacitor => async dispatch => { await deleteKapacitorAJAX(kapacitor) dispatch(deleteKapacitor(kapacitor)) } catch (err) { - dispatch( - publishNotification( - 'error', - 'Internal Server Error. Could not delete Kapacitor config.' - ) - ) + dispatch(notify(NOTIFY_COULD_NOT_DELETE_KAPACITOR)) } } diff --git a/ui/src/shared/apis/index.js b/ui/src/shared/apis/index.js index a01ee2b9b4..18ec7d602f 100644 --- a/ui/src/shared/apis/index.js +++ b/ui/src/shared/apis/index.js @@ -36,20 +36,31 @@ export function deleteSource(source) { }) } -export function pingKapacitor(kapacitor) { - return AJAX({ - method: 'GET', - url: kapacitor.links.ping, - }) +export const pingKapacitor = async kapacitor => { + try { + const data = await AJAX({ + method: 'GET', + url: kapacitor.links.ping, + }) + return data + } catch (error) { + console.error(error) + throw error + } } -export function getKapacitor(source, kapacitorID) { - return AJAX({ - url: `${source.links.kapacitors}/${kapacitorID}`, - method: 'GET', - }).then(({data}) => { +export const getKapacitor = async (source, kapacitorID) => { + try { + const {data} = await AJAX({ + url: `${source.links.kapacitors}/${kapacitorID}`, + method: 'GET', + }) + return data - }) + } catch (error) { + console.error(error) + throw error + } } export const getActiveKapacitor = async source => { @@ -93,7 +104,7 @@ export const deleteKapacitor = async kapacitor => { export function createKapacitor( source, - {url, name = 'My Kapacitor', username, password} + {url, name = 'My Kapacitor', username, password, insecureSkipVerify} ) { return AJAX({ url: source.links.kapacitors, @@ -103,6 +114,7 @@ export function createKapacitor( url, username, password, + insecureSkipVerify, }, }) } @@ -114,6 +126,7 @@ export function updateKapacitor({ username, password, active, + insecureSkipVerify, }) { return AJAX({ url: links.self, @@ -124,12 +137,18 @@ export function updateKapacitor({ username, password, active, + insecureSkipVerify, }, }) } export const getKapacitorConfig = async kapacitor => { - return await kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/config', '') + try { + return await kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/config', '') + } catch (error) { + console.error(error) + throw error + } } export const getKapacitorConfigSection = (kapacitor, section) => { diff --git a/ui/src/shared/components/Crosshair.js b/ui/src/shared/components/Crosshair.js index a111dd35df..d6afa5163c 100644 --- a/ui/src/shared/components/Crosshair.js +++ b/ui/src/shared/components/Crosshair.js @@ -1,4 +1,5 @@ -import React, {PropTypes, Component} from 'react' +import React, {Component} from 'react' +import PropTypes from 'prop-types' import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'shared/constants' import {NULL_HOVER_TIME} from 'shared/constants/tableGraph' diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index 350845b938..9f6629ae19 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -13,7 +13,7 @@ import Annotations from 'src/shared/components/Annotations' import Crosshair from 'src/shared/components/Crosshair' import getRange, {getStackedRange} from 'shared/parsing/getRangeForDygraph' -import {DISPLAY_OPTIONS} from 'src/dashboards/constants' +import {AXES_SCALE_OPTIONS} from 'src/dashboards/constants/cellEditor' import {buildDefaultYLabel} from 'shared/presenters' import {numberValueFormatter} from 'src/utils/formatting' import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph' @@ -26,7 +26,7 @@ import { hasherino, highlightSeriesOpts, } from 'src/shared/graphs/helpers' -const {LINEAR, LOG, BASE_10, BASE_2} = DISPLAY_OPTIONS +const {LINEAR, LOG, BASE_10, BASE_2} = AXES_SCALE_OPTIONS class Dygraph extends Component { constructor(props) { diff --git a/ui/src/shared/components/Gauge.js b/ui/src/shared/components/Gauge.js index 704fa37fb0..52ea1614eb 100644 --- a/ui/src/shared/components/Gauge.js +++ b/ui/src/shared/components/Gauge.js @@ -8,7 +8,7 @@ import { COLOR_TYPE_MIN, COLOR_TYPE_MAX, MIN_THRESHOLDS, -} from 'src/dashboards/constants/gaugeColors' +} from 'shared/constants/thresholds' class Gauge extends Component { constructor(props) { diff --git a/ui/src/shared/components/GaugeChart.js b/ui/src/shared/components/GaugeChart.js index d280df2072..6ca9157d06 100644 --- a/ui/src/shared/components/GaugeChart.js +++ b/ui/src/shared/components/GaugeChart.js @@ -3,11 +3,9 @@ import PropTypes from 'prop-types' import lastValues from 'shared/parsing/lastValues' import Gauge from 'shared/components/Gauge' -import { - DEFAULT_GAUGE_COLORS, - stringifyColorValues, -} from 'src/dashboards/constants/gaugeColors' -import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants' +import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds' +import {stringifyColorValues} from 'src/shared/constants/colorOperations' +import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants' class GaugeChart extends PureComponent { render() { diff --git a/ui/src/shared/components/InputClickToEdit.js b/ui/src/shared/components/InputClickToEdit.js deleted file mode 100644 index 6d6daa1699..0000000000 --- a/ui/src/shared/components/InputClickToEdit.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' - -class InputClickToEdit extends Component { - constructor(props) { - super(props) - - this.state = { - isEditing: null, - value: this.props.value, - } - } - - handleCancel = () => { - this.setState({ - isEditing: false, - value: this.props.value, - }) - } - - handleInputClick = () => { - this.setState({isEditing: true}) - } - - handleInputBlur = e => { - const {onUpdate, value} = this.props - - if (value !== e.target.value) { - onUpdate(e.target.value) - } - - this.setState({isEditing: false, value: e.target.value}) - } - - handleKeyDown = e => { - if (e.key === 'Enter') { - this.handleInputBlur(e) - } - if (e.key === 'Escape') { - this.handleCancel() - } - } - - handleFocus = e => { - e.target.select() - } - - render() { - const {isEditing, value} = this.state - const { - wrapperClass: wrapper, - disabled, - tabIndex, - placeholder, - appearAsNormalInput, - } = this.props - - const wrapperClass = `${wrapper}${appearAsNormalInput - ? ' input-cte__normal' - : ''}` - const defaultStyle = value ? 'input-cte' : 'input-cte__empty' - - return disabled - ?
-
- {value} -
-
- :
- {isEditing - ? (this.inputRef = r)} - tabIndex={tabIndex} - spellCheck={false} - /> - :
- {value || placeholder} - {appearAsNormalInput || } -
} -
- } -} - -const {func, bool, number, string} = PropTypes - -InputClickToEdit.propTypes = { - wrapperClass: string.isRequired, - value: string, - onUpdate: func.isRequired, - disabled: bool, - tabIndex: number, - placeholder: string, - appearAsNormalInput: bool, -} - -export default InputClickToEdit diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index 0b55d27e03..39eaebdeaf 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -41,7 +41,7 @@ const Layout = ( { host, cell, - cell: {h, axes, type, colors, legend}, + cell: {h, axes, type, colors, legend, tableOptions}, source, sources, onZoom, @@ -79,6 +79,7 @@ const Layout = ( inView={cell.inView} axes={axes} type={type} + tableOptions={tableOptions} staticLegend={IS_STATIC_LEGEND(legend)} cellHeight={h} onZoom={onZoom} diff --git a/ui/src/shared/components/Notification.js b/ui/src/shared/components/Notification.js new file mode 100644 index 0000000000..2002fb9d94 --- /dev/null +++ b/ui/src/shared/components/Notification.js @@ -0,0 +1,99 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import classnames from 'classnames' + +import {dismissNotification as dismissNotificationAction} from 'shared/actions/notifications' + +import {NOTIFICATION_TRANSITION} from 'shared/constants/index' + +class Notification extends Component { + constructor(props) { + super(props) + + this.state = { + opacity: 1, + height: 0, + dismissed: false, + } + } + + componentDidMount() { + const {notification: {duration}} = this.props + + // Trigger animation in + const {height} = this.notificationRef.getBoundingClientRect() + this.setState({height}) + + if (duration >= 0) { + // Automatically dismiss notification after duration prop + this.dismissTimer = setTimeout(this.handleDismiss, duration) + } + } + + componentWillUnmount() { + clearTimeout(this.dismissTimer) + clearTimeout(this.deleteTimer) + } + + handleDismiss = () => { + const {notification: {id}, dismissNotification} = this.props + + this.setState({dismissed: true}) + this.deleteTimer = setTimeout( + () => dismissNotification(id), + NOTIFICATION_TRANSITION + ) + } + + render() { + const {notification: {type, message, icon}} = this.props + const {height, dismissed} = this.state + + const notificationContainerClass = classnames('notification-container', { + show: !!height, + 'notification-dismissed': dismissed, + }) + const notificationClass = `notification notification-${type}` + const notificationMargin = 4 + + return ( +
+
(this.notificationRef = r)} + > + +
+ {message} +
+
+
+ ) + } +} + +const {func, number, shape, string} = PropTypes + +Notification.propTypes = { + notification: shape({ + id: string.isRequired, + type: string.isRequired, + message: string.isRequired, + duration: number.isRequired, + icon: string.isRequired, + }).isRequired, + dismissNotification: func.isRequired, +} + +const mapDispatchToProps = dispatch => ({ + dismissNotification: bindActionCreators(dismissNotificationAction, dispatch), +}) + +export default connect(null, mapDispatchToProps)(Notification) diff --git a/ui/src/shared/components/Notifications.js b/ui/src/shared/components/Notifications.js index 43e4ab2231..201b64b17a 100644 --- a/ui/src/shared/components/Notifications.js +++ b/ui/src/shared/components/Notifications.js @@ -1,111 +1,39 @@ -import React, {Component} from 'react' +import React from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' -import {withRouter} from 'react-router' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' -import {getNotificationID} from 'src/shared/reducers/notifications' +import Notification from 'shared/components/Notification' -import { - publishNotification as publishNotificationAction, - dismissNotification as dismissNotificationAction, - dismissAllNotifications as dismissAllNotificationsAction, -} from 'shared/actions/notifications' +const Notifications = ({notifications, inPresentationMode}) => +
+ {notifications.map(n => )} +
-class Notifications extends Component { - constructor(props) { - super(props) - } - - componentWillReceiveProps(nextProps) { - if (nextProps.location.pathname !== this.props.location.pathname) { - this.props.dismissAllNotifications() - } - } - - renderNotification = (type, message) => { - const isDismissed = this.props.dismissedNotifications[ - getNotificationID(message, type) - ] - if (!message || isDismissed) { - return null - } - const cls = classnames('alert', { - 'alert-danger': type === 'error', - 'alert-success': type === 'success', - 'alert-warning': type === 'warning', - }) - return ( -
- {message} - {this.renderDismiss(type)} -
- ) - } - - handleDismiss = type => () => this.props.dismissNotification(type) - - renderDismiss = type => { - return ( - - ) - } - - render() { - const {success, error, warning} = this.props.notifications - if (!success && !error && !warning) { - return null - } - - return ( -
- {this.renderNotification('success', success)} - {this.renderNotification('error', error)} - {this.renderNotification('warning', warning)} -
- ) - } -} - -const {func, shape, string} = PropTypes +const {arrayOf, bool, number, shape, string} = PropTypes Notifications.propTypes = { - location: shape({ - pathname: string.isRequired, - }).isRequired, - publishNotification: func.isRequired, - dismissNotification: func.isRequired, - dismissAllNotifications: func.isRequired, - notifications: shape({ - success: string, - error: string, - warning: string, - }), - dismissedNotifications: shape({}), + notifications: arrayOf( + shape({ + id: string.isRequired, + type: string.isRequired, + message: string.isRequired, + duration: number.isRequired, + icon: string, + }) + ), + inPresentationMode: bool, } -const mapStateToProps = ({notifications, dismissedNotifications}) => ({ +const mapStateToProps = ({ notifications, - dismissedNotifications, + app: {ephemeral: {inPresentationMode}}, +}) => ({ + notifications, + inPresentationMode, }) -const mapDispatchToProps = dispatch => ({ - publishNotification: bindActionCreators(publishNotificationAction, dispatch), - dismissNotification: bindActionCreators(dismissNotificationAction, dispatch), - dismissAllNotifications: bindActionCreators( - dismissAllNotificationsAction, - dispatch - ), -}) - -export default connect(mapStateToProps, mapDispatchToProps)( - withRouter(Notifications) -) +export default connect(mapStateToProps, null)(Notifications) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index a406327c72..5d732a38ce 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -22,6 +22,7 @@ const RefreshingGraph = ({ onZoom, cellID, queries, + tableOptions, templates, timeRange, cellHeight, @@ -95,6 +96,7 @@ const RefreshingGraph = ({ resizerTopHeight={resizerTopHeight} resizeCoords={resizeCoords} cellID={cellID} + tableOptions={tableOptions} hoverTime={hoverTime} onSetHoverTime={onSetHoverTime} inView={inView} @@ -163,6 +165,7 @@ RefreshingGraph.propTypes = { ), cellID: string, inView: bool, + tableOptions: shape({}), } RefreshingGraph.defaultProps = { diff --git a/ui/src/shared/components/SingleStat.js b/ui/src/shared/components/SingleStat.js index a560ddf462..9ea2a9cf83 100644 --- a/ui/src/shared/components/SingleStat.js +++ b/ui/src/shared/components/SingleStat.js @@ -5,8 +5,7 @@ import lastValues from 'shared/parsing/lastValues' import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers' import {DYGRAPH_CONTAINER_V_MARGIN} from 'shared/constants' -import {SINGLE_STAT_TEXT} from 'src/dashboards/constants/gaugeColors' -import {generateSingleStatHexs} from 'shared/constants/colorOperations' +import {generateThresholdsListHexs} from 'shared/constants/colorOperations' class SingleStat extends PureComponent { render() { @@ -33,13 +32,11 @@ class SingleStat extends PureComponent { const lastValue = lastValues(data)[1] const precision = 100.0 const roundedValue = Math.round(+lastValue * precision) / precision - const colorizeText = !!colors.find(color => color.type === SINGLE_STAT_TEXT) - const {bgColor, textColor} = generateSingleStatHexs( + const {bgColor, textColor} = generateThresholdsListHexs( colors, - lineGraph, - colorizeText, - lastValue + lastValue, + lineGraph ) const backgroundColor = bgColor diff --git a/ui/src/shared/components/TableGraph.js b/ui/src/shared/components/TableGraph.js index d215c893de..32203cd27b 100644 --- a/ui/src/shared/components/TableGraph.js +++ b/ui/src/shared/components/TableGraph.js @@ -3,14 +3,18 @@ import PropTypes from 'prop-types' import _ from 'lodash' import classnames from 'classnames' +import {MultiGrid} from 'react-virtualized' +import moment from 'moment' + import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph' import { NULL_COLUMN_INDEX, NULL_ROW_INDEX, NULL_HOVER_TIME, + TIME_FORMAT_DEFAULT, + TIME_COLUMN_DEFAULT, } from 'src/shared/constants/tableGraph' - -import {MultiGrid} from 'react-virtualized' +import {generateThresholdsListHexs} from 'shared/constants/colorOperations' const isEmpty = data => data.length <= 1 @@ -59,14 +63,24 @@ class TableGraph extends Component { } } - cellRenderer = ({columnIndex, rowIndex, key, style, parent}) => { + cellRenderer = ({columnIndex, rowIndex, key, parent, style}) => { const data = this._data const {hoveredColumnIndex, hoveredRowIndex} = this.state + const {colors} = this.props const columnCount = _.get(data, ['0', 'length'], 0) const rowCount = data.length + const {tableOptions} = this.props + const timeFormat = tableOptions + ? tableOptions.timeFormat + : TIME_FORMAT_DEFAULT + const columnNames = tableOptions + ? tableOptions.columnNames + : [TIME_COLUMN_DEFAULT] + const isFixedRow = rowIndex === 0 && columnIndex > 0 const isFixedColumn = rowIndex > 0 && columnIndex === 0 + const isTimeData = isFixedColumn const isFixedCorner = rowIndex === 0 && columnIndex === 0 const isLastRow = rowIndex === rowCount - 1 const isLastColumn = columnIndex === columnCount - 1 @@ -74,7 +88,22 @@ class TableGraph extends Component { rowIndex === parent.props.scrollToRow || (rowIndex === hoveredRowIndex && hoveredRowIndex !== 0) || (columnIndex === hoveredColumnIndex && hoveredColumnIndex !== 0) - const dataIsNumerical = _.isNumber([rowIndex][columnIndex]) + const dataIsNumerical = _.isNumber(data[rowIndex][columnIndex]) + + let cellStyle = style + + if (!isFixedRow && !isFixedColumn && !isFixedCorner) { + const {bgColor, textColor} = generateThresholdsListHexs( + colors, + data[rowIndex][columnIndex] + ) + + cellStyle = { + ...style, + backgroundColor: bgColor, + color: textColor, + } + } const cellClass = classnames('table-graph-cell', { 'table-graph-cell__fixed-row': isFixedRow, @@ -86,21 +115,30 @@ class TableGraph extends Component { 'table-graph-cell__numerical': dataIsNumerical, }) + const cellData = data[rowIndex][columnIndex] + const foundColumn = columnNames.find( + column => column.internalName === cellData + ) + const columnName = + foundColumn && (foundColumn.displayName || foundColumn.internalName) + return (
- {`${data[rowIndex][columnIndex]}`} + {isTimeData + ? `${moment(cellData).format(timeFormat)}` + : columnName || `${cellData}`}
) } render() { const {hoveredColumnIndex, hoveredRowIndex} = this.state - const {hoverTime} = this.props + const {hoverTime, tableOptions, colors} = this.props const data = this._data const columnCount = _.get(data, ['0', 'length'], 0) const rowCount = data.length @@ -128,11 +166,18 @@ class TableGraph extends Component { fixedRowCount={1} enableFixedColumnScroll={true} enableFixedRowScroll={true} + timeFormat={ + tableOptions ? tableOptions.timeFormat : TIME_FORMAT_DEFAULT + } + columnNames={ + tableOptions ? tableOptions.columnNames : [TIME_COLUMN_DEFAULT] + } scrollToRow={hoverTimeRow} cellRenderer={this.cellRenderer} hoveredColumnIndex={hoveredColumnIndex} hoveredRowIndex={hoveredRowIndex} hoverTime={hoverTime} + colors={colors} />} ) @@ -144,8 +189,18 @@ const {arrayOf, number, shape, string, func} = PropTypes TableGraph.propTypes = { cellHeight: number, data: arrayOf(shape()), + tableOptions: shape({}), hoverTime: string, onSetHoverTime: func, + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), } export default TableGraph diff --git a/ui/src/shared/components/ThresholdsList.js b/ui/src/shared/components/ThresholdsList.js new file mode 100644 index 0000000000..95385fcbc0 --- /dev/null +++ b/ui/src/shared/components/ThresholdsList.js @@ -0,0 +1,199 @@ +import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import _ from 'lodash' +import uuid from 'uuid' + +import Threshold from 'src/dashboards/components/Threshold' +import ColorDropdown from 'shared/components/ColorDropdown' + +import {updateThresholdsListColors} from 'src/dashboards/actions/cellEditorOverlay' + +import { + THRESHOLD_COLORS, + DEFAULT_VALUE_MIN, + DEFAULT_VALUE_MAX, + MAX_THRESHOLDS, + THRESHOLD_TYPE_BASE, +} from 'shared/constants/thresholds' + +const formatColor = color => { + const {hex, name} = color + return {hex, name} +} + +class ThresholdsList extends Component { + handleAddThreshold = () => { + const { + thresholdsListColors, + thresholdsListType, + handleUpdateThresholdsListColors, + onResetFocus, + } = this.props + + const randomColor = _.random(0, THRESHOLD_COLORS.length - 1) + + const maxValue = DEFAULT_VALUE_MIN + const minValue = DEFAULT_VALUE_MAX + + let randomValue = _.round(_.random(minValue, maxValue, true), 2) + + if (thresholdsListColors.length > 0) { + const colorsValues = _.mapValues(thresholdsListColors, 'value') + do { + randomValue = _.round(_.random(minValue, maxValue, true), 2) + } while (_.includes(colorsValues, randomValue)) + } + + const newThreshold = { + type: thresholdsListType, + id: uuid.v4(), + value: randomValue, + hex: THRESHOLD_COLORS[randomColor].hex, + name: THRESHOLD_COLORS[randomColor].name, + } + + const updatedColors = _.sortBy( + [...thresholdsListColors, newThreshold], + color => color.value + ) + + handleUpdateThresholdsListColors(updatedColors) + onResetFocus() + } + + handleDeleteThreshold = threshold => () => { + const { + handleUpdateThresholdsListColors, + onResetFocus, + thresholdsListColors, + } = this.props + const updatedThresholdsListColors = thresholdsListColors.filter( + color => color.id !== threshold.id + ) + const sortedColors = _.sortBy( + updatedThresholdsListColors, + color => color.value + ) + + handleUpdateThresholdsListColors(sortedColors) + onResetFocus() + } + + handleChooseColor = threshold => chosenColor => { + const {handleUpdateThresholdsListColors} = this.props + + const thresholdsListColors = this.props.thresholdsListColors.map( + color => + color.id === threshold.id + ? {...color, hex: chosenColor.hex, name: chosenColor.name} + : color + ) + + handleUpdateThresholdsListColors(thresholdsListColors) + } + + handleUpdateColorValue = (threshold, value) => { + const {handleUpdateThresholdsListColors} = this.props + + const thresholdsListColors = this.props.thresholdsListColors.map( + color => (color.id === threshold.id ? {...color, value} : color) + ) + + handleUpdateThresholdsListColors(thresholdsListColors) + } + + handleValidateColorValue = (threshold, targetValue) => { + const {thresholdsListColors} = this.props + const sortedColors = _.sortBy(thresholdsListColors, color => color.value) + + return !sortedColors.some(color => color.value === targetValue) + } + + handleSortColors = () => { + const {thresholdsListColors, handleUpdateThresholdsListColors} = this.props + const sortedColors = _.sortBy(thresholdsListColors, color => color.value) + + handleUpdateThresholdsListColors(sortedColors) + } + + render() { + const {thresholdsListColors, showListHeading} = this.props + const disableAddThreshold = thresholdsListColors.length > MAX_THRESHOLDS + + const thresholdsListClass = `thresholds-list${showListHeading && + ' graph-options-group'}` + + return ( +
+ {showListHeading && } + + {thresholdsListColors.map( + color => + color.id === THRESHOLD_TYPE_BASE + ?
+
Base Color
+ +
+ : + )} +
+ ) + } +} +const {arrayOf, bool, func, number, shape, string} = PropTypes + +ThresholdsList.defaultProps = { + showListHeading: false, +} +ThresholdsList.propTypes = { + thresholdsListType: string.isRequired, + thresholdsListColors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: number.isRequired, + }).isRequired + ), + handleUpdateThresholdsListColors: func.isRequired, + onResetFocus: func.isRequired, + showListHeading: bool, +} + +const mapStateToProps = ({ + cellEditorOverlay: {thresholdsListType, thresholdsListColors}, +}) => ({ + thresholdsListType, + thresholdsListColors, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateThresholdsListColors: bindActionCreators( + updateThresholdsListColors, + dispatch + ), +}) +export default connect(mapStateToProps, mapDispatchToProps)(ThresholdsList) diff --git a/ui/src/shared/components/ThresholdsListTypeToggle.js b/ui/src/shared/components/ThresholdsListTypeToggle.js new file mode 100644 index 0000000000..931877fafa --- /dev/null +++ b/ui/src/shared/components/ThresholdsListTypeToggle.js @@ -0,0 +1,67 @@ +import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import {updateThresholdsListType} from 'src/dashboards/actions/cellEditorOverlay' + +import { + THRESHOLD_TYPE_TEXT, + THRESHOLD_TYPE_BG, +} from 'shared/constants/thresholds' + +class ThresholdsListTypeToggle extends Component { + handleToggleThresholdsListType = newType => () => { + const {handleUpdateThresholdsListType} = this.props + + handleUpdateThresholdsListType(newType) + } + + render() { + const {thresholdsListType, containerClass} = this.props + + return ( +
+ +
    +
  • + Background +
  • +
  • + Text +
  • +
+
+ ) + } +} +const {func, string} = PropTypes + +ThresholdsListTypeToggle.propTypes = { + thresholdsListType: string.isRequired, + handleUpdateThresholdsListType: func.isRequired, + containerClass: string.isRequired, +} + +const mapStateToProps = ({cellEditorOverlay: {thresholdsListType}}) => ({ + thresholdsListType, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateThresholdsListType: bindActionCreators( + updateThresholdsListType, + dispatch + ), +}) +export default connect(mapStateToProps, mapDispatchToProps)( + ThresholdsListTypeToggle +) diff --git a/ui/src/shared/constants/colorOperations.js b/ui/src/shared/constants/colorOperations.js index b1dbf6ba0b..6056b735f5 100644 --- a/ui/src/shared/constants/colorOperations.js +++ b/ui/src/shared/constants/colorOperations.js @@ -1,8 +1,9 @@ import _ from 'lodash' import { - GAUGE_COLORS, - SINGLE_STAT_BASE, -} from 'src/dashboards/constants/gaugeColors' + THRESHOLD_COLORS, + THRESHOLD_TYPE_BASE, + THRESHOLD_TYPE_TEXT, +} from 'shared/constants/thresholds' const hexToRgb = hex => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) @@ -41,20 +42,24 @@ const findNearestCrossedThreshold = (colors, lastValue) => { return nearestCrossedThreshold } -export const generateSingleStatHexs = ( +export const stringifyColorValues = colors => { + return colors.map(color => ({...color, value: `${color.value}`})) +} + +export const generateThresholdsListHexs = ( colors, - containsLineGraph, - colorizeText, - lastValue + lastValue, + containsLineGraph ) => { - const defaultColoring = {bgColor: null, textColor: GAUGE_COLORS[11].hex} + const defaultColoring = {bgColor: null, textColor: THRESHOLD_COLORS[11].hex} + const lastValueNumber = Number(lastValue) || 0 if (!colors.length || !lastValue) { return defaultColoring } // baseColor is expected in all cases - const baseColor = colors.find(color => (color.id = SINGLE_STAT_BASE)) || { + const baseColor = colors.find(color => (color.id = THRESHOLD_TYPE_BASE)) || { hex: defaultColoring.textColor, } @@ -66,17 +71,20 @@ export const generateSingleStatHexs = ( } // When there is only a base color and it's applied to the text - if (colorizeText && colors.length === 1) { + const shouldColorizeText = !!colors.find( + color => color.type === THRESHOLD_TYPE_TEXT + ) + if (shouldColorizeText && colors.length === 1) { return baseColor ? {bgColor: null, textColor: baseColor.hex} : defaultColoring } // When there's multiple colors and they're applied to the text - if (colorizeText && colors.length > 1) { + if (shouldColorizeText && colors.length > 1) { const nearestCrossedThreshold = findNearestCrossedThreshold( colors, - lastValue + lastValueNumber ) const bgColor = null const textColor = nearestCrossedThreshold.hex @@ -96,7 +104,7 @@ export const generateSingleStatHexs = ( if (colors.length > 1) { const nearestCrossedThreshold = findNearestCrossedThreshold( colors, - lastValue + lastValueNumber ) const bgColor = nearestCrossedThreshold diff --git a/ui/src/shared/constants/index.js b/ui/src/shared/constants/index.js index 39fbc58b01..3309981eda 100644 --- a/ui/src/shared/constants/index.js +++ b/ui/src/shared/constants/index.js @@ -386,9 +386,6 @@ export const DROPDOWN_MENU_MAX_HEIGHT = 240 export const HEARTBEAT_INTERVAL = 10000 // ms export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds. -export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds. - -export const NOTIFICATION_DISMISS_DELAY = 4000 // in milliseconds export const REVERT_STATE_DELAY = 1500 // ms @@ -445,3 +442,8 @@ export const cellSupportsAnnotations = cellType => { ] return !!supportedTypes.find(type => type === cellType) } + +export const NOTIFICATION_TRANSITION = 250 +export const FIVE_SECONDS = 5000 +export const TEN_SECONDS = 10000 +export const INFINITE = -1 diff --git a/ui/src/shared/constants/tableGraph.js b/ui/src/shared/constants/tableGraph.js index 717d015751..ffa2694f82 100644 --- a/ui/src/shared/constants/tableGraph.js +++ b/ui/src/shared/constants/tableGraph.js @@ -2,3 +2,28 @@ export const NULL_COLUMN_INDEX = -1 export const NULL_ROW_INDEX = -1 export const NULL_HOVER_TIME = '0' + +export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.ss' +export const TIME_FORMAT_CUSTOM = 'Custom' + +export const TIME_COLUMN_DEFAULT = {internalName: 'time', displayName: ''} + +export const FORMAT_OPTIONS = [ + {text: TIME_FORMAT_DEFAULT}, + {text: 'MM/DD/YYYY HH:mm'}, + {text: 'MM/DD/YYYY'}, + {text: 'h:mm:ss A'}, + {text: 'h:mm A'}, + {text: 'MMMM D, YYYY'}, + {text: 'MMMM D, YYYY h:mm A'}, + {text: 'dddd, MMMM D, YYYY h:mm A'}, + {text: TIME_FORMAT_CUSTOM}, +] + +export const DEFAULT_TABLE_OPTIONS = { + timeFormat: 'MM/DD/YYYY HH:mm:ss.ss', + verticalTimeAxis: true, + sortBy: TIME_COLUMN_DEFAULT, + wrapping: 'truncate', + columnNames: [TIME_COLUMN_DEFAULT], +} diff --git a/ui/src/dashboards/constants/gaugeColors.js b/ui/src/shared/constants/thresholds.js similarity index 72% rename from ui/src/dashboards/constants/gaugeColors.js rename to ui/src/shared/constants/thresholds.js index 2419a3d471..0d366ebcb5 100644 --- a/ui/src/dashboards/constants/gaugeColors.js +++ b/ui/src/shared/constants/thresholds.js @@ -9,11 +9,13 @@ export const COLOR_TYPE_MAX = 'max' export const DEFAULT_VALUE_MAX = 100 export const COLOR_TYPE_THRESHOLD = 'threshold' -export const SINGLE_STAT_TEXT = 'text' -export const SINGLE_STAT_BG = 'background' -export const SINGLE_STAT_BASE = 'base' +export const THRESHOLD_TYPE_TEXT = 'text' +export const THRESHOLD_TYPE_BG = 'background' +export const THRESHOLD_TYPE_BASE = 'base' -export const GAUGE_COLORS = [ +export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.ss' + +export const THRESHOLD_COLORS = [ { hex: '#BF3D5E', name: 'ruby', @@ -95,49 +97,49 @@ export const GAUGE_COLORS = [ export const DEFAULT_GAUGE_COLORS = [ { type: COLOR_TYPE_MIN, - hex: GAUGE_COLORS[11].hex, + hex: THRESHOLD_COLORS[11].hex, id: '0', - name: GAUGE_COLORS[11].name, + name: THRESHOLD_COLORS[11].name, value: DEFAULT_VALUE_MIN, }, { type: COLOR_TYPE_MAX, - hex: GAUGE_COLORS[14].hex, + hex: THRESHOLD_COLORS[14].hex, id: '1', - name: GAUGE_COLORS[14].name, + name: THRESHOLD_COLORS[14].name, value: DEFAULT_VALUE_MAX, }, ] -export const DEFAULT_SINGLESTAT_COLORS = [ +export const DEFAULT_THRESHOLDS_LIST_COLORS = [ { - type: SINGLE_STAT_TEXT, - hex: GAUGE_COLORS[11].hex, - id: SINGLE_STAT_BASE, - name: GAUGE_COLORS[11].name, + type: THRESHOLD_TYPE_TEXT, + hex: THRESHOLD_COLORS[11].hex, + id: THRESHOLD_TYPE_BASE, + name: THRESHOLD_COLORS[11].name, value: -999999999999999999, }, ] export const DEFAULT_TABLE_COLORS = [ { - type: SINGLE_STAT_BG, - hex: GAUGE_COLORS[18].hex, - id: SINGLE_STAT_BASE, - name: GAUGE_COLORS[18].name, + type: THRESHOLD_TYPE_BG, + hex: THRESHOLD_COLORS[18].hex, + id: THRESHOLD_TYPE_BASE, + name: THRESHOLD_COLORS[18].name, value: 0, }, ] -export const validateSingleStatColors = (colors, type) => { +export const validateThresholdsListColors = (colors, type) => { if (!colors || colors.length === 0) { - return DEFAULT_SINGLESTAT_COLORS + return DEFAULT_THRESHOLDS_LIST_COLORS } let containsBaseColor = false const formattedColors = colors.map(color => { - if (color.id === SINGLE_STAT_BASE) { + if (color.id === THRESHOLD_TYPE_BASE) { // Check for existance of base color containsBaseColor = true return {...color, value: Number(color.value), type} @@ -148,22 +150,22 @@ export const validateSingleStatColors = (colors, type) => { const formattedColorsWithBase = [ ...formattedColors, - DEFAULT_SINGLESTAT_COLORS[0], + DEFAULT_THRESHOLDS_LIST_COLORS[0], ] return containsBaseColor ? formattedColors : formattedColorsWithBase } -export const getSingleStatType = colors => { +export const getThresholdsListType = colors => { const type = _.get(colors, ['0', 'type'], false) if (type) { - if (_.includes([SINGLE_STAT_TEXT, SINGLE_STAT_BG], type)) { + if (_.includes([THRESHOLD_TYPE_TEXT, THRESHOLD_TYPE_BG], type)) { return type } } - return SINGLE_STAT_TEXT + return THRESHOLD_TYPE_TEXT } export const validateGaugeColors = colors => { @@ -185,7 +187,3 @@ export const validateGaugeColors = colors => { return formattedColors } - -export const stringifyColorValues = colors => { - return colors.map(color => ({...color, value: `${color.value}`})) -} diff --git a/ui/src/shared/copy/notifications.js b/ui/src/shared/copy/notifications.js new file mode 100644 index 0000000000..19f1a2cd63 --- /dev/null +++ b/ui/src/shared/copy/notifications.js @@ -0,0 +1,532 @@ +// All copy for notifications should be stored here for easy editing +// and ensuring stylistic consistency + +import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'shared/constants/index' + +const defaultErrorNotification = { + type: 'error', + icon: 'alert-triangle', + duration: TEN_SECONDS, +} + +const defaultSuccessNotification = { + type: 'success', + icon: 'checkmark', + duration: FIVE_SECONDS, +} + +// Misc Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_GENERIC_FAIL = 'Could not communicate with server.' + +export const NOTIFY_NEW_VERSION = message => ({ + type: 'info', + icon: 'cubo-uniform', + duration: INFINITE, + message, +}) + +export const NOTIFY_ERR_WITH_ALT_TEXT = (type, message) => ({ + type, + icon: 'triangle', + duration: TEN_SECONDS, + message, +}) + +export const NOTIFY_PRESENTATION_MODE = { + type: 'primary', + icon: 'expand-b', + duration: 7500, + message: 'Press ESC to exit Presentation Mode.', +} + +export const NOTIFY_DATA_WRITTEN = { + ...defaultSuccessNotification, + message: 'Data was written successfully.', +} + +export const NOTIFY_SESSION_TIMED_OUT = { + type: 'primary', + icon: 'triangle', + duration: INFINITE, + message: 'Your session has timed out. Log in again to continue.', +} + +export const NOTIFY_SERVER_ERROR = { + ...defaultErrorNotification, + mesasage: 'Internal Server Error. Check API Logs.', +} + +export const NOTIFY_COULD_NOT_RETRIEVE_KAPACITORS = sourceID => ({ + ...defaultErrorNotification, + mesasage: `Internal Server Error. Could not retrieve Kapacitor Connections for source ${sourceID}.`, +}) + +export const NOTIFY_COULD_NOT_DELETE_KAPACITOR = { + ...defaultErrorNotification, + message: 'Internal Server Error. Could not delete Kapacitor Connection.', +} + +// Hosts Page Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_UNABLE_TO_GET_HOSTS = { + ...defaultErrorNotification, + message: 'Unable to get Hosts.', +} + +export const NOTIFY_UNABLE_TO_GET_APPS = { + ...defaultErrorNotification, + message: 'Unable to get Apps for Hosts.', +} + +// InfluxDB Sources Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_SOURCE_CREATION_SUCCEEDED = sourceName => ({ + ...defaultSuccessNotification, + icon: 'server2', + message: `Connected to InfluxDB ${sourceName} successfully.`, +}) + +export const NOTIFY_SOURCE_CREATION_FAILED = (sourceName, errorMessage) => ({ + ...defaultErrorNotification, + icon: 'server2', + message: `Unable to connect to InfluxDB ${sourceName}: ${errorMessage}`, +}) + +export const NOTIFY_SOURCE_UPDATED = sourceName => ({ + ...defaultSuccessNotification, + icon: 'server2', + message: `Updated InfluxDB ${sourceName} Connection successfully.`, +}) + +export const NOTIFY_SOURCE_UPDATE_FAILED = (sourceName, errorMessage) => ({ + ...defaultErrorNotification, + icon: 'server2', + message: `Failed to update InfluxDB ${sourceName} Connection: ${errorMessage}`, +}) + +export const NOTIFY_SOURCE_DELETED = sourceName => ({ + ...defaultSuccessNotification, + icon: 'server2', + message: `${sourceName} deleted successfully.`, +}) + +export const NOTIFY_SOURCE_DELETE_FAILED = sourceName => ({ + ...defaultErrorNotification, + icon: 'server2', + message: `There was a problem deleting ${sourceName}.`, +}) + +export const NOTIFY_SOURCE_NO_LONGER_AVAILABLE = sourceName => + `Source ${sourceName} is no longer available. Successfully connected to another source.` + +export const NOTIFY_NO_SOURCES_AVAILABLE = sourceName => + `Unable to connect to source ${sourceName}. No other sources available.` + +export const NOTIFY_UNABLE_TO_RETRIEVE_SOURCES = 'Unable to retrieve sources.' + +export const NOTIFY_UNABLE_TO_CONNECT_SOURCE = sourceName => + `Unable to connect to source ${sourceName}.` + +export const NOTIFY_ERROR_CONNECTING_TO_SOURCE = errorMessage => + `Unable to connect to InfluxDB source: ${errorMessage}` + +// Multitenancy User Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_USER_REMOVED_FROM_ALL_ORGS = { + ...defaultErrorNotification, + duration: INFINITE, + message: + 'You have been removed from all organizations. Please contact your administrator.', +} + +export const NOTIFY_USER_REMOVED_FROM_CURRENT_ORG = { + ...defaultErrorNotification, + duration: INFINITE, + message: 'You were removed from your current organization.', +} + +export const NOTIFY_ORG_HAS_NO_SOURCES = { + ...defaultErrorNotification, + duration: INFINITE, + message: 'Organization has no sources configured.', +} + +export const NOTIFY_USER_SWITCHED_ORGS = (orgName, roleName) => ({ + ...defaultSuccessNotification, + type: 'primary', + message: `Now logged in to '${orgName}' as '${roleName}'.`, +}) + +export const NOTIFY_ORG_IS_PRIVATE = { + ...defaultErrorNotification, + duration: INFINITE, + message: + 'This organization is private. To gain access, you must be explicitly added by an administrator.', +} + +export const NOTIFY_CURRENT_ORG_DELETED = { + ...defaultErrorNotification, + duration: INFINITE, + message: 'Your current organization was deleted.', +} + +// Chronograf Admin Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_MAPPING_DELETED = (id, scheme) => ({ + ...defaultSuccessNotification, + message: `Mapping ${id}/${scheme} deleted successfully.`, +}) + +export const NOTIFY_CHRONOGRAF_USER_ADDED_TO_ORG = (user, organization) => + `${user} has been added to ${organization} successfully.` + +export const NOTIFY_CHRONOGRAF_USER_REMOVED_FROM_ORG = (user, organization) => + `${user} has been removed from ${organization} successfully.` + +export const NOTIFY_CHRONOGRAF_USER_UPDATED = message => ({ + ...defaultSuccessNotification, + message, +}) + +export const NOTIFY_CHRONOGRAF_ORG_DELETED = orgName => ({ + ...defaultSuccessNotification, + message: `Organization ${orgName} deleted successfully.`, +}) + +export const NOTIFY_CHRONOGRAF_USER_DELETED = (user, isAbsoluteDelete) => ({ + ...defaultSuccessNotification, + message: `${user} has been removed from ${isAbsoluteDelete + ? 'all organizations and deleted.' + : 'the current organization.'}`, +}) + +export const NOTIFY_CHRONOGRAF_USER_MISSING_NAME_AND_PROVIDER = { + ...defaultErrorNotification, + type: 'warning', + message: 'User must have a Name and Provider.', +} + +// InfluxDB Admin Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_DB_USER_CREATED = { + ...defaultSuccessNotification, + message: 'User created successfully.', +} + +export const NOTIFY_DB_USER_CREATION_FAILED = errorMessage => + `Failed to create User: ${errorMessage}` + +export const NOTIFY_DB_USER_DELETED = userName => ({ + ...defaultSuccessNotification, + message: `User "${userName}" deleted successfully.`, +}) + +export const NOTIFY_DB_USER_DELETION_FAILED = errorMessage => + `Failed to delete User: ${errorMessage}` + +export const NOTIFY_DB_USER_PERMISSIONS_UPDATED = { + ...defaultSuccessNotification, + message: 'User Permissions updated successfully.', +} + +export const NOTIFY_DB_USER_PERMISSIONS_UPDATE_FAILED = errorMessage => + `Failed to update User Permissions: ${errorMessage}` + +export const NOTIFY_DB_USER_ROLES_UPDATED = { + ...defaultSuccessNotification, + message: 'User Roles updated successfully.', +} + +export const NOTIFY_DB_USER_ROLES_UPDATE_FAILED = errorMessage => + `Failed to update User Roles: ${errorMessage}` + +export const NOTIFY_DB_USER_PASSWORD_UPDATED = { + ...defaultSuccessNotification, + message: 'User Password updated successfully.', +} + +export const NOTIFY_DB_USER_PASSWORD_UPDATE_FAILED = errorMessage => + `Failed to update User Password: ${errorMessage}` + +export const NOTIFY_DATABASE_CREATED = { + ...defaultSuccessNotification, + message: 'Database created successfully.', +} + +export const NOTIFY_DATABASE_CREATION_FAILED = errorMessage => + `Failed to create Database: ${errorMessage}` + +export const NOTIFY_DATABASE_DELETED = databaseName => ({ + ...defaultSuccessNotification, + message: `Database "${databaseName}" deleted successfully.`, +}) + +export const NOTIFY_DATABASE_DELETION_FAILED = errorMessage => + `Failed to delete Database: ${errorMessage}` + +export const NOTIFY_ROLE_CREATED = { + ...defaultSuccessNotification, + message: 'Role created successfully.', +} + +export const NOTIFY_ROLE_CREATION_FAILED = errorMessage => + `Failed to create Role: ${errorMessage}` + +export const NOTIFY_ROLE_DELETED = roleName => ({ + ...defaultSuccessNotification, + message: `Role "${roleName}" deleted successfully.`, +}) + +export const NOTIFY_ROLE_DELETION_FAILED = errorMessage => + `Failed to delete Role: ${errorMessage}` + +export const NOTIFY_ROLE_USERS_UPDATED = { + ...defaultSuccessNotification, + message: 'Role Users updated successfully.', +} + +export const NOTIFY_ROLE_USERS_UPDATE_FAILED = errorMessage => + `Failed to update Role Users: ${errorMessage}` + +export const NOTIFY_ROLE_PERMISSIONS_UPDATED = { + ...defaultSuccessNotification, + message: 'Role Permissions updated successfully.', +} + +export const NOTIFY_ROLE_PERMISSIONS_UPDATE_FAILED = errorMessage => + `Failed to update Role Permissions: ${errorMessage}` + +export const NOTIFY_RETENTION_POLICY_CREATED = { + ...defaultSuccessNotification, + message: 'Retention Policy created successfully.', +} + +export const NOTIFY_RETENTION_POLICY_CREATION_FAILED = errorMessage => + `Failed to create Retention Policy: ${errorMessage}` + +export const NOTIFY_RETENTION_POLICY_DELETED = rpName => ({ + ...defaultSuccessNotification, + message: `Retention Policy "${rpName}" deleted successfully.`, +}) + +export const NOTIFY_RETENTION_POLICY_DELETION_FAILED = errorMessage => + `Failed to delete Retention Policy: ${errorMessage}` + +export const NOTIFY_RETENTION_POLICY_UPDATED = { + ...defaultSuccessNotification, + message: 'Retention Policy updated successfully.', +} + +export const NOTIFY_RETENTION_POLICY_UPDATE_FAILED = errorMessage => + `Failed to update Retention Policy: ${errorMessage}` + +export const NOTIFY_QUERIES_ERROR = errorMessage => ({ + ...defaultErrorNotification, + errorMessage, +}) + +export const NOTIFY_RETENTION_POLICY_CANT_HAVE_EMPTY_FIELDS = { + ...defaultErrorNotification, + message: 'Fields cannot be empty.', +} + +export const NOTIFY_DATABASE_DELETE_CONFIRMATION_REQUIRED = databaseName => ({ + ...defaultErrorNotification, + message: `Type "DELETE ${databaseName}" to confirm.`, +}) + +export const NOTIFY_DB_USER_NAME_PASSWORD_INVALID = { + ...defaultErrorNotification, + message: 'Username and/or Password too short.', +} + +export const NOTIFY_ROLE_NAME_INVALID = { + ...defaultErrorNotification, + message: 'Role name is too short.', +} + +export const NOTIFY_DATABASE_NAME_INVALID = { + ...defaultErrorNotification, + message: 'Database name cannot be blank.', +} + +export const NOTIFY_DATABASE_NAME_ALREADY_EXISTS = { + ...defaultErrorNotification, + message: 'A Database by this name already exists.', +} + +// Dashboard Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_TEMP_VAR_ALREADY_EXISTS = tempVarName => ({ + ...defaultErrorNotification, + icon: 'cube', + message: `Variable '${tempVarName}' already exists. Please enter a new value.`, +}) + +export const NOTIFY_DASHBOARD_NOT_FOUND = dashboardID => ({ + ...defaultErrorNotification, + icon: 'dash-h', + message: `Dashboard ${dashboardID} could not be found`, +}) + +export const NOTIFY_DASHBOARD_DELETED = name => ({ + ...defaultSuccessNotification, + icon: 'dash-h', + message: `Dashboard ${name} deleted successfully.`, +}) + +export const NOTIFY_DASHBOARD_DELETE_FAILED = (name, errorMessage) => + `Failed to delete Dashboard ${name}: ${errorMessage}.` + +// Rule Builder Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_ALERT_RULE_CREATED = { + ...defaultSuccessNotification, + message: 'Alert Rule created successfully.', +} + +export const NOTIFY_ALERT_RULE_CREATION_FAILED = { + ...defaultErrorNotification, + message: 'Alert Rule could not be created.', +} + +export const NOTIFY_ALERT_RULE_UPDATED = ruleName => ({ + ...defaultSuccessNotification, + message: `${ruleName} saved successfully.`, +}) + +export const NOTIFY_ALERT_RULE_UPDATE_FAILED = (ruleName, errorMessage) => ({ + ...defaultErrorNotification, + message: `There was a problem saving ${ruleName}: ${errorMessage}`, +}) + +export const NOTIFY_ALERT_RULE_DELETED = ruleName => ({ + ...defaultSuccessNotification, + message: `${ruleName} deleted successfully.`, +}) + +export const NOTIFY_ALERT_RULE_DELETION_FAILED = ruleName => ({ + ...defaultErrorNotification, + message: `${ruleName} could not be deleted.`, +}) + +export const NOTIFY_ALERT_RULE_STATUS_UPDATED = (ruleName, updatedStatus) => ({ + ...defaultSuccessNotification, + message: `${ruleName} ${updatedStatus} successfully.`, +}) + +export const NOTIFY_ALERT_RULE_STATUS_UPDATE_FAILED = ( + ruleName, + updatedStatus +) => ({ + ...defaultSuccessNotification, + message: `${ruleName} could not be ${updatedStatus}.`, +}) + +export const NOTIFY_ALERT_RULE_REQUIRES_QUERY = + 'Please select a Database, Measurement, and Field.' + +export const NOTIFY_ALERT_RULE_REQUIRES_CONDITION_VALUE = + 'Please enter a value in the Conditions section.' + +export const NOTIFY_ALERT_RULE_DEADMAN_INVALID = + 'Deadman rules require a Database and Measurement.' + +// Kapacitor Configuration Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_KAPACITOR_NAME_ALREADY_TAKEN = kapacitorName => ({ + ...defaultErrorNotification, + message: `There is already a Kapacitor Connection named "${kapacitorName}".`, +}) + +export const NOTIFY_COULD_NOT_FIND_KAPACITOR = { + ...defaultErrorNotification, + message: 'We could not find a Kapacitor configuration for this source.', +} + +export const NOTIFY_REFRESH_KAPACITOR_FAILED = { + ...defaultErrorNotification, + message: 'There was an error getting the Kapacitor configuration.', +} + +export const NOTIFY_ALERT_ENDPOINT_SAVED = endpoint => ({ + ...defaultSuccessNotification, + message: `Alert configuration for ${endpoint} saved successfully.`, +}) + +export const NOTIFY_ALERT_ENDPOINT_SAVE_FAILED = (endpoint, errorMessage) => ({ + ...defaultErrorNotification, + message: `There was an error saving the alert configuration for ${endpoint}: ${errorMessage}`, +}) + +export const NOTIFY_TEST_ALERT_SENT = endpoint => ({ + ...defaultSuccessNotification, + duration: TEN_SECONDS, + message: `Test Alert sent to ${endpoint}. If the Alert does not reach its destination, please check your endpoint configuration settings.`, +}) + +export const NOTIFY_TEST_ALERT_FAILED = (endpoint, errorMessage) => ({ + ...defaultErrorNotification, + message: `There was an error sending a Test Alert to ${endpoint}${errorMessage + ? `: ${errorMessage}` + : '.'}`, +}) + +export const NOTIFY_KAPACITOR_CONNECTION_FAILED = { + ...defaultErrorNotification, + message: + 'Could not connect to Kapacitor. Check your connection settings in the Configuration page.', +} + +export const NOTIFY_KAPACITOR_CREATED = { + ...defaultSuccessNotification, + message: + 'Connected to Kapacitor successfully! Configuring endpoints is optional.', +} + +export const NOTIFY_KAPACITOR_CREATION_FAILED = { + ...defaultErrorNotification, + message: 'There was a problem connecting to Kapacitor.', +} + +export const NOTIFY_KAPACITOR_UPDATED = { + ...defaultSuccessNotification, + message: 'Kapacitor Connection updated successfully.', +} + +export const NOTIFY_KAPACITOR_UPDATE_FAILED = { + ...defaultErrorNotification, + message: 'There was a problem updating the Kapacitor Connection.', +} + +// TICKscript Notifications +// ---------------------------------------------------------------------------- +export const NOTIFY_TICKSCRIPT_CREATED = { + ...defaultSuccessNotification, + message: 'TICKscript successfully created.', +} + +export const NOTIFY_TICKSCRIPT_CREATION_FAILED = 'Failed to create TICKscript.' + +export const NOTIFY_TICKSCRIPT_UPDATED = { + ...defaultSuccessNotification, + message: 'TICKscript successfully updated.', +} + +export const NOTIFY_TICKSCRIPT_UPDATE_FAILED = 'Failed to update TICKscript.' + +export const NOTIFY_TICKSCRIPT_LOGGING_UNAVAILABLE = { + type: 'warning', + icon: 'alert-triangle', + duration: INFINITE, + message: 'Kapacitor version 1.4 required to view TICKscript logs', +} + +export const NOTIFY_TICKSCRIPT_LOGGING_ERROR = message => ({ + ...defaultErrorNotification, + message, +}) + +export const NOTIFY_KAPACITOR_NOT_FOUND = + 'We could not find a Kapacitor configuration for this source.' diff --git a/ui/src/shared/dispatchers/index.js b/ui/src/shared/dispatchers/index.js index 9a813330e3..fc8019d8ae 100644 --- a/ui/src/shared/dispatchers/index.js +++ b/ui/src/shared/dispatchers/index.js @@ -1,34 +1,8 @@ -import { - publishNotification, - dismissNotification, -} from 'shared/actions/notifications' +import {notify} from 'shared/actions/notifications' import {delayEnablePresentationMode} from 'shared/actions/app' - -import {PRESENTATION_MODE_NOTIFICATION_DELAY} from 'shared/constants' -import {NOTIFICATION_DISMISS_DELAY} from 'shared/constants' - -export function delayDismissNotification(type, delay) { - return dispatch => { - setTimeout(() => dispatch(dismissNotification(type)), delay) - } -} - -export const publishAutoDismissingNotification = ( - type, - message, - delay = NOTIFICATION_DISMISS_DELAY -) => dispatch => { - dispatch(publishNotification(type, message)) - dispatch(delayDismissNotification(type, delay)) -} +import {NOTIFY_PRESENTATION_MODE} from 'shared/copy/notifications' export const presentationButtonDispatcher = dispatch => () => { dispatch(delayEnablePresentationMode()) - dispatch( - publishAutoDismissingNotification( - 'success', - 'Press ESC to disable presentation mode.', - PRESENTATION_MODE_NOTIFICATION_DELAY - ) - ) + dispatch(notify(NOTIFY_PRESENTATION_MODE)) } diff --git a/ui/src/shared/middleware/errors.js b/ui/src/shared/middleware/errors.js index ebfaf24156..c72965b15a 100644 --- a/ui/src/shared/middleware/errors.js +++ b/ui/src/shared/middleware/errors.js @@ -1,15 +1,22 @@ import _ from 'lodash' import {authExpired} from 'shared/actions/auth' -import {publishNotification as notify} from 'shared/actions/notifications' +import {notify} from 'shared/actions/notifications' import {HTTP_FORBIDDEN} from 'shared/constants' +import { + NOTIFY_SESSION_TIMED_OUT, + NOTIFY_ERR_WITH_ALT_TEXT, + NOTIFY_NEW_VERSION, + NOTIFY_ORG_IS_PRIVATE, + NOTIFY_CURRENT_ORG_DELETED, +} from 'shared/copy/notifications' const actionsAllowedDuringBlackout = [ '@@', 'AUTH_', 'ME_', - 'NOTIFICATION_', + 'PUBLISH_NOTIFICATION', 'ERROR_', 'LINKS_', ] @@ -20,7 +27,7 @@ const errorsMiddleware = store => next => action => { const {auth: {me}} = store.getState() if (action.type === 'ERROR_THROWN') { - const {error, error: {status, auth}, altText, alertType = 'error'} = action + const {error, error: {status, auth}, altText, alertType = 'info'} = action if (status === HTTP_FORBIDDEN) { const message = _.get(error, 'data.message', '') @@ -35,25 +42,22 @@ const errorsMiddleware = store => next => action => { message === `This organization is private. To gain access, you must be explicitly added by an administrator.` // eslint-disable-line quotes ) { - store.dispatch(notify(alertType, message)) + store.dispatch(notify(NOTIFY_ORG_IS_PRIVATE)) + } + + if (_.startsWith(message, 'Welcome to Chronograf')) { + store.dispatch(notify(NOTIFY_NEW_VERSION(message))) } if (organizationWasRemoved) { - store.dispatch( - notify(alertType, 'Your current organization was deleted.') - ) + store.dispatch(notify(NOTIFY_CURRENT_ORG_DELETED)) allowNotifications = false setTimeout(() => { allowNotifications = true }, notificationsBlackoutDuration) } else if (wasSessionTimeout) { - store.dispatch( - notify( - alertType, - 'Your session has timed out. Log in again to continue.' - ) - ) + store.dispatch(notify(NOTIFY_SESSION_TIMED_OUT)) allowNotifications = false setTimeout(() => { @@ -61,10 +65,10 @@ const errorsMiddleware = store => next => action => { }, notificationsBlackoutDuration) } } else if (altText) { - store.dispatch(notify(alertType, altText)) + store.dispatch(notify(NOTIFY_ERR_WITH_ALT_TEXT(alertType, altText))) } else { // TODO: actually do proper error handling - // store.dispatch(notify(alertType, 'Cannot communicate with server.')) + // store.dispatch(notify({type: alertType, 'Cannot communicate with server.')) } } diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index 40ad9a5690..5264d7450f 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -3,7 +3,7 @@ import auth from './auth' import config from './config' import errors from './errors' import links from './links' -import {notifications, dismissedNotifications} from './notifications' +import {notifications} from './notifications' import sources from './sources' import annotations from './annotations' @@ -16,5 +16,4 @@ export default { sources, annotations, notifications, - dismissedNotifications, } diff --git a/ui/src/shared/reducers/notifications.js b/ui/src/shared/reducers/notifications.js index 6767a6f289..e132f779c3 100644 --- a/ui/src/shared/reducers/notifications.js +++ b/ui/src/shared/reducers/notifications.js @@ -1,47 +1,21 @@ -import u from 'updeep' -import _ from 'lodash' +import uuid from 'uuid' +export const initialState = [] -export const notifications = (state = {}, action) => { +export const notifications = (state = initialState, action) => { switch (action.type) { - case 'NOTIFICATION_RECEIVED': { - const {type, message} = action.payload - return u.updateIn(type, message, state) - } - case 'NOTIFICATION_DISMISSED': { - const {type} = action.payload - return u(u.omit(type), state) - } - case 'ALL_NOTIFICATIONS_DISMISSED': { - // Reset to initial state - return {} - } - } - - return state -} - -export const getNotificationID = (message, type) => _.snakeCase(message) + type - -export const dismissedNotifications = (state = {}, action) => { - switch (action.type) { - case 'NOTIFICATION_RECEIVED': { - const {type, message, once} = action.payload - if (once) { - // Create a message ID in a deterministic way, also with its type - const messageID = getNotificationID(message, type) - if (state[messageID]) { - // Message action called with once option but we've already seen it - return state - } - // Message action called with once option and it's not present on - // the persisted state - return { - ...state, - [messageID]: true, - } + case 'PUBLISH_NOTIFICATION': { + const {notification} = action.payload + const publishedNotification = { + ...notification, + id: uuid.v4(), } - // Message action not called with once option - return state + + return [publishedNotification, ...state] + } + + case 'DISMISS_NOTIFICATION': { + const {id} = action.payload + return state.filter(n => n.id !== id) } } diff --git a/ui/src/sources/containers/ManageSources.js b/ui/src/sources/containers/ManageSources.js index cca6b5ff6a..2368cd1910 100644 --- a/ui/src/sources/containers/ManageSources.js +++ b/ui/src/sources/containers/ManageSources.js @@ -9,11 +9,17 @@ import { setActiveKapacitorAsync, deleteKapacitorAsync, } from 'shared/actions/sources' +import {notify as notifyAction} from 'shared/actions/notifications' import FancyScrollbar from 'shared/components/FancyScrollbar' import SourceIndicator from 'shared/components/SourceIndicator' import InfluxTable from 'src/sources/components/InfluxTable' +import { + NOTIFY_SOURCE_DELETED, + NOTIFY_SOURCE_DELETE_FAILED, +} from 'shared/copy/notifications' + const V_NUMBER = VERSION // eslint-disable-line no-undef class ManageSources extends Component { @@ -36,19 +42,13 @@ class ManageSources extends Component { } handleDeleteSource = source => () => { - const {addFlashMessage} = this.props + const {notify} = this.props try { this.props.removeAndLoadSources(source) - addFlashMessage({ - type: 'success', - text: `Deleted source ${source.name}`, - }) + notify(NOTIFY_SOURCE_DELETED(source.name)) } catch (e) { - addFlashMessage({ - type: 'error', - text: 'Could not remove source from Chronograf', - }) + notify(NOTIFY_SOURCE_DELETE_FAILED(source.name)) } } @@ -101,7 +101,7 @@ ManageSources.propTypes = { }), }), sources: array, - addFlashMessage: func, + notify: func.isRequired, removeAndLoadSources: func.isRequired, fetchKapacitors: func.isRequired, setActiveKapacitor: func.isRequired, @@ -117,6 +117,7 @@ const mapDispatchToProps = dispatch => ({ fetchKapacitors: bindActionCreators(fetchKapacitorsAsync, dispatch), setActiveKapacitor: bindActionCreators(setActiveKapacitorAsync, dispatch), deleteKapacitor: bindActionCreators(deleteKapacitorAsync, dispatch), + notify: bindActionCreators(notifyAction, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(ManageSources) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.js index 812004952c..8c813e76fb 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.js @@ -8,8 +8,9 @@ import { addSource as addSourceAction, updateSource as updateSourceAction, } from 'shared/actions/sources' -import {publishNotification} from 'shared/actions/notifications' +import {notify as notifyAction} from 'shared/actions/notifications' import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import Notifications from 'shared/components/Notifications' import SourceForm from 'src/sources/components/SourceForm' @@ -18,6 +19,14 @@ import SourceIndicator from 'shared/components/SourceIndicator' import {DEFAULT_SOURCE} from 'shared/constants' const initialPath = '/sources/new' +import { + NOTIFY_ERROR_CONNECTING_TO_SOURCE, + NOTIFY_SOURCE_CREATION_SUCCEEDED, + NOTIFY_SOURCE_CREATION_FAILED, + NOTIFY_SOURCE_UPDATED, + NOTIFY_SOURCE_UPDATE_FAILED, +} from 'shared/copy/notifications' + class SourcePage extends Component { constructor(props) { super(props) @@ -32,7 +41,7 @@ class SourcePage extends Component { componentDidMount() { const {editMode} = this.state - const {params} = this.props + const {params, notify} = this.props if (!editMode) { return this.setState({isLoading: false}) @@ -46,7 +55,7 @@ class SourcePage extends Component { }) }) .catch(error => { - this.handleError('Could not connect to source', error) + notify(NOTIFY_ERROR_CONNECTING_TO_SOURCE(this._parseError(error))) this.setState({isLoading: false}) }) } @@ -95,13 +104,6 @@ class SourcePage extends Component { this.setState(this._normalizeSource, this._updateSource) } - handleError = (bannerText, err) => { - const {notify} = this.props - const error = this._parseError(err) - console.error('Error: ', error) - notify('error', `${bannerText}: ${error}`) - } - gotoPurgatory = () => { const {router} = this.props router.push('/purgatory') @@ -123,7 +125,7 @@ class SourcePage extends Component { } createSource(source) .then(({data: sourceFromServer}) => { - this.props.addSourceAction(sourceFromServer) + this.props.addSource(sourceFromServer) this.setState({ source: {...DEFAULT_SOURCE, ...sourceFromServer}, isCreated: true, @@ -141,12 +143,14 @@ class SourcePage extends Component { const {notify} = this.props createSource(source) .then(({data: sourceFromServer}) => { - this.props.addSourceAction(sourceFromServer) + this.props.addSource(sourceFromServer) this._redirect(sourceFromServer) - notify('success', `InfluxDB ${source.name} available as a connection`) + notify(NOTIFY_SOURCE_CREATION_SUCCEEDED(source.name)) }) .catch(error => { - this.handleError('Unable to create InfluxDB connection', error) + notify( + NOTIFY_SOURCE_CREATION_FAILED(source.name, this._parseError(error)) + ) }) } @@ -155,12 +159,14 @@ class SourcePage extends Component { const {notify} = this.props updateSource(source) .then(({data: sourceFromServer}) => { - this.props.updateSourceAction(sourceFromServer) + this.props.updateSource(sourceFromServer) this._redirect(sourceFromServer) - notify('success', `InfluxDB connection ${source.name} updated`) + notify(NOTIFY_SOURCE_UPDATED(source.name)) }) .catch(error => { - this.handleError('Unable to update InfluxDB connection', error) + notify( + NOTIFY_SOURCE_UPDATE_FAILED(source.name, this._parseError(error)) + ) }) } @@ -261,15 +267,14 @@ SourcePage.propTypes = { redirectPath: string, }).isRequired, }).isRequired, - notify: func, - addSourceAction: func, - updateSourceAction: func, + notify: func.isRequired, + addSource: func.isRequired, + updateSource: func.isRequired, } -const mapStateToProps = () => ({}) - -export default connect(mapStateToProps, { - notify: publishNotification, - addSourceAction, - updateSourceAction, -})(withRouter(SourcePage)) +const mapDispatchToProps = dispatch => ({ + notify: bindActionCreators(notifyAction, dispatch), + addSource: bindActionCreators(addSourceAction, dispatch), + updateSource: bindActionCreators(updateSourceAction, dispatch), +}) +export default connect(null, mapDispatchToProps)(withRouter(SourcePage)) diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index ccb2c7f1b4..cb6e656715 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -29,7 +29,6 @@ @import 'layout/page-header'; @import 'layout/sidebar'; @import 'layout/overlay'; -@import 'layout/flash-messages'; // Components @import 'components/annotations'; @@ -69,6 +68,7 @@ @import 'components/source-selector'; @import 'components/tables'; @import 'components/table-graph'; +@import 'components/threshold-controls'; @import 'components/kapacitor-logs-table'; // Pages diff --git a/ui/src/style/components/ceo-display-options.scss b/ui/src/style/components/ceo-display-options.scss index 16b26f1a13..8bfd67d9f4 100644 --- a/ui/src/style/components/ceo-display-options.scss +++ b/ui/src/style/components/ceo-display-options.scss @@ -280,4 +280,8 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold { width: calc(100% + 12px); margin-left: -6px; margin-right: -6px; + + .form-group:last-of-type { + margin-bottom: 0; + } } diff --git a/ui/src/style/components/threshold-controls.scss b/ui/src/style/components/threshold-controls.scss new file mode 100644 index 0000000000..96e13ced63 --- /dev/null +++ b/ui/src/style/components/threshold-controls.scss @@ -0,0 +1,60 @@ +/* + Threshold Controls + ------------------------------------------------------------------------------ + Used primarily within the Cell Editor Overlay for Single Stat, Gauge, + and Table type cells +*/ + +.thresholds-list { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.threshold-item { + display: flex; + flex-wrap: nowrap; + align-items: center; + height: 30px; + margin-top: 8px; + + > * { + margin-left: 4px; + + &:first-child { + margin-left: 0; + } + } +} + +%threshold-item--label-styles { + height: 30px; + line-height: 30px; + font-weight: 600; + font-size: 13px; + padding: 0 11px; + border-radius: 4px; + @include no-user-select(); +} + +.threshold-item--label { + @extend %threshold-item--label-styles; + color: $g11-sidewalk; + background-color: $g4-onyx; + width: 120px; +} + +.threshold-item--label__editable { + @extend %threshold-item--label-styles; + color: $g16-pearl; + width: 90px; +} + +.threshold-item--input { + flex: 1 0 0; +} + +.threshold-item .color-dropdown.color-dropdown--stretch { + width: auto; + flex: 1 0 0; +} diff --git a/ui/src/style/layout/flash-messages.scss b/ui/src/style/layout/flash-messages.scss deleted file mode 100644 index ec1fcf5f1c..0000000000 --- a/ui/src/style/layout/flash-messages.scss +++ /dev/null @@ -1,12 +0,0 @@ -/* - Flash Messages - ---------------------------------------------------------------------------- -*/ -.flash-messages { - position: fixed; - left: 50%; - transform: translateX(-50%); - width: 570px; - top: 36px; - z-index: 9999; -} diff --git a/ui/src/style/theme/_alerts.scss b/ui/src/style/theme/_alerts.scss deleted file mode 100644 index 952be73c0e..0000000000 --- a/ui/src/style/theme/_alerts.scss +++ /dev/null @@ -1,98 +0,0 @@ -/* - Alerts - ----------------------------------------------------------------------------- -*/ - -.alert { - border-style: solid; - border-width: 0; - border-radius: $ix-radius; - position: relative; - padding: $ix-marg-c; - margin-bottom: $ix-marg-c; - font-weight: 500; - @extend %no-user-select; - - button.close { - outline: none; - position: absolute; - top: 50%; - transform: translateY(-50%); - right: ($ix-marg-c - $ix-marg-a); - font-size: $ix-text-base; - width: 20px; - height: 20px; - opacity: 0.25; - transition: - opacity 0.25s ease; - - &:hover { - opacity: 1; - } - } - - &-icon { - padding-left: ($ix-marg-e - $ix-marg-b); - - span.icon:not(.remove) { - position: absolute; - top: 50%; - left: $ix-marg-c; - transform: translateY(-50%); - width: ($ix-text-base-2 + 4px); - margin: 0; - font-size: $ix-text-base-2; - } - } -} - -// Mixin for Alert Themes -// ---------------------------------------------------------------------------- -@mixin alert-styles( - $bg-color, - $bg-color-2, - $text-color, - $link-color, - $link-hover) { - font-size: 16px; - - @include gradient-h($bg-color,$bg-color-2); - color: $text-color; - - a:link, - a:visited { - color: $link-color; - font-weight: 700; - text-decoration: underline; - transition: - color 0.25s ease; - } - a:hover { - color: $link-hover; - border-color: $link-hover; - } - span.icon { - color: $text-color; - } -} - -// Alert Themes -// ---------------------------------------------------------------------------- -.alert-success { - @include alert-styles($c-rainforest,$c-pool,$g20-white,$c-wasabi,$g20-white); -} -.alert-primary { - @include alert-styles($c-pool,$c-ocean,$g20-white,$c-neutrino,$g20-white); -} -.alert-warning { - @include alert-styles($c-star,$c-pool,$g20-white,$c-neutrino,$g20-white); -} -.alert-danger { - @include alert-styles($c-curacao,$c-star,$g20-white,$c-marmelade,$g20-white); -} -.alert-info { - @include alert-styles($g20-white,$g16-pearl,$g8-storm,$ix-link-default,$ix-link-default-hover); -} -.alert-dark { - @include alert-styles($c-sapphire,$c-shadow,$c-moonstone,$ix-link-default,$ix-link-default-hover); -} diff --git a/ui/src/style/theme/_notifications.scss b/ui/src/style/theme/_notifications.scss new file mode 100644 index 0000000000..aae627edcb --- /dev/null +++ b/ui/src/style/theme/_notifications.scss @@ -0,0 +1,154 @@ +/* + Notifications + ----------------------------------------------------------------------------- +*/ + +$notification-margin: 12px; + +.notification-center { + position: fixed; + right: $notification-margin; + width: 360px; + top: $chronograf-page-header-height + $notification-margin; + z-index: 9999; +} + +.notification-center__presentation-mode { + @extend .notification-center; + top: $notification-margin; +} + +.notification { + border-style: solid; + border-width: 0; + border-radius: $ix-radius; + position: relative; + padding: 12px 40px; + @extend %no-user-select; + transform: translateX(105%); + transition: + transform 0.25s ease 0.25s, + opacity 0.25s ease; + + > span.icon { + position: absolute; + top: 50%; + left: 20px; + transform: translate(-50%,-50%); + font-size: $ix-text-base-2; + } +} + +.notification-message { + font-weight: 500; + font-size: 14px; + line-height: 16px; +} + +.notification-close { + outline: none; + position: absolute; + top: 50%; + border: 0; + background-color: transparent; + transform: translateY(-50%); + right: ($ix-marg-c - $ix-marg-a); + font-size: $ix-text-base; + width: 20px; + height: 20px; + opacity: 0.25; + transition: + opacity 0.25s ease; + + &:before, + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 2px; + border-radius: 1px; + background-color: $g20-white; + } + &:before { + transform: translate(-50%,-50%) rotate(-45deg); + } + &:after { + transform: translate(-50%,-50%) rotate(45deg); + } + + &:hover { + cursor: pointer; + opacity: 1; + } +} +.notification-container { + overflow: hidden; + height: 0; + transition: height 0.25s ease; + + &.show .notification { + transform: translateX(0); + } + &.notification-dismissed { + height: 0 !important; + .notification {opacity: 0;} + } +} + + +// Mixin for Alert Themes +// ---------------------------------------------------------------------------- +@mixin notification-styles( + $bg-color, + $bg-color-2, + $text-color, + $link-color, + $link-hover) { + font-size: 16px; + + @include gradient-h($bg-color,$bg-color-2); + color: $text-color; + + a:link, + a:visited { + color: $link-color; + font-weight: 700; + text-decoration: underline; + transition: + color 0.25s ease; + } + a:hover { + color: $link-hover; + border-color: $link-hover; + } + span.icon { + color: $text-color; + } + .notification-close:before, + .notification-close:after { + background-color: $text-color; + } +} + +// Alert Themes +// ---------------------------------------------------------------------------- +.notification-success { + @include notification-styles($c-rainforest,$c-pool,$g20-white,$c-wasabi,$g20-white); +} +.notification-primary { + @include notification-styles($c-pool,$c-ocean,$g20-white,$c-neutrino,$g20-white); +} +.notification-warning { + @include notification-styles($c-star,$c-pool,$g20-white,$c-neutrino,$g20-white); +} +.notification-error { + @include notification-styles($c-curacao,$c-star,$g20-white,$c-marmelade,$g20-white); +} +.notification-info { + @include notification-styles($g20-white,$g16-pearl,$g8-storm,$ix-link-default,$ix-link-default-hover); +} +.notification-dark { + @include notification-styles($c-sapphire,$c-shadow,$c-moonstone,$ix-link-default,$ix-link-default-hover); +} diff --git a/ui/src/style/theme/chronograf-theme.scss b/ui/src/style/theme/chronograf-theme.scss index f04955190f..4cbc991e00 100644 --- a/ui/src/style/theme/chronograf-theme.scss +++ b/ui/src/style/theme/chronograf-theme.scss @@ -12,7 +12,7 @@ @import 'tables'; @import 'dropdowns'; @import 'form-elements'; -@import 'alerts'; +@import 'notifications'; @import 'panels'; @import 'radio-buttons'; @import 'misc'; diff --git a/ui/src/types/auth.tsx b/ui/src/types/auth.tsx new file mode 100644 index 0000000000..62ca59ca69 --- /dev/null +++ b/ui/src/types/auth.tsx @@ -0,0 +1,54 @@ +export interface Organization { + defaultRole: string + id: string + links: { + self: string + } + name: string +} + +export interface Role { + name: string + organization: string +} + +export interface User { + id: string + links: {self: string} + name: string + provider: string + roles: Role[] + scheme: string + superAdmin: boolean +} + +export interface Auth { + callback: string + label: string + login: string + logout: string + name: string +} + +export interface AuthConfig { + auth: string + self: string +} + +export interface AuthLinks { + allUsers: string + auth: Auth[] + config: AuthConfig + dashboards: string + environment: string + external: { + statusFeed?: string + } + layouts: string + logout: string + mappings: string + me: string + organizations: string + sources: string + users: string +} diff --git a/ui/src/types/index.tsx b/ui/src/types/index.tsx index 618e7410e9..e758eb1f32 100644 --- a/ui/src/types/index.tsx +++ b/ui/src/types/index.tsx @@ -1,5 +1,6 @@ -import { Kapacitor, AlertRule } from "./kapacitor" -import { Query, QueryConfig } from "./query" -import { Source } from "./sources" +import {AuthLinks, Role, User, Organization} from './auth' +import {AlertRule, Kapacitor} from "./kapacitor" +import {Query, QueryConfig} from "./query" +import {Source} from "./sources" -export { Kapacitor, AlertRule, Query, QueryConfig, Source } +export {AuthLinks, Role, User, Organization, AlertRule, Kapacitor, Query, QueryConfig, Source, } diff --git a/ui/src/types/kapacitor.tsx b/ui/src/types/kapacitor.tsx index f97fde7c4d..c4bb8bdec5 100644 --- a/ui/src/types/kapacitor.tsx +++ b/ui/src/types/kapacitor.tsx @@ -7,6 +7,7 @@ export interface Kapacitor { username?: string password?: string active: boolean + insecureSkipVerify: boolean links: { self: string } diff --git a/ui/src/types/sources.tsx b/ui/src/types/sources.tsx index fbde1da606..ac77477426 100644 --- a/ui/src/types/sources.tsx +++ b/ui/src/types/sources.tsx @@ -25,4 +25,4 @@ interface SourceLinks { users: string databases: string roles?: string -} +} \ No newline at end of file diff --git a/ui/test/admin/containers/chronograf/AllUsersPage.test.tsx b/ui/test/admin/containers/chronograf/AllUsersPage.test.tsx new file mode 100644 index 0000000000..c109a07d1f --- /dev/null +++ b/ui/test/admin/containers/chronograf/AllUsersPage.test.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import {AllUsersPage} from 'src/admin/containers/chronograf/AllUsersPage' +import {shallow} from 'enzyme' + +import {authLinks as links} from 'test/resources' + +const noop = () => {} + +const setup = (override = {}) => { + const props = { + links, + meID: '1', + users: [], + organizations: [], + actionsAdmin: { + loadUsersAsync: noop, + loadOrganizationsAsync: noop, + createUserAsync: noop, + updateUserAsync: noop, + deleteUserAsync: noop, + }, + actionsConfig: { + getAuthConfigAsync: noop, + updateAuthConfigAsync: noop, + }, + authConfig: { + superAdminNewUsers: false, + }, + notify: noop, + ...override, + } + + const wrapper = shallow() + + return { + wrapper, + props, + } +} + +describe('Admin.Containers.Chronograf.AllUsersPage', () => { + describe('rendering', () => { + it('renders', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + }) +}) diff --git a/ui/test/dashboards/components/GraphOptionsCustomizableColumn.test.tsx b/ui/test/dashboards/components/GraphOptionsCustomizableColumn.test.tsx new file mode 100644 index 0000000000..477807e29d --- /dev/null +++ b/ui/test/dashboards/components/GraphOptionsCustomizableColumn.test.tsx @@ -0,0 +1,62 @@ +import React from 'react' + +import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn' +import InputClickToEdit from 'src/shared/components/InputClickToEdit' + +import {shallow} from 'enzyme' + +const setup = (override = {}) => { + const props = { + internalName: '', + displayName: '', + onColumnRename: () => {}, + ...override, + } + + const wrapper = shallow() + const instance = wrapper.instance() as GraphOptionsCustomizableColumn + + return {wrapper, props, instance} +} + +describe('Dashboards.Components.GraphOptionsCustomizableColumn', () => { + describe('rendering', () => { + it('displays both label div and InputClickToEdit', () => { + const {wrapper} = setup() + const label = wrapper.find('div').last() + const input = wrapper.find(InputClickToEdit) + + expect(label.exists()).toBe(true) + expect(input.exists()).toBe(true) + }) + + describe('when there is an internalName', () => { + it('displays the value', () => { + const internalName = 'test' + const {wrapper} = setup({internalName}) + const label = wrapper.find('div').last() + expect(label.exists()).toBe(true) + expect(label.children().contains(internalName)).toBe(true) + }) + }) + }) + + describe('instance methods', () => { + describe('#handleColumnRename', () => { + it('calls onColumnRename once', () => { + const onColumnRename = jest.fn() + const internalName = 'test' + const {instance} = setup({onColumnRename, internalName}) + const rename = 'TEST' + + instance.handleColumnRename(rename) + + expect(onColumnRename).toHaveBeenCalledTimes(1) + expect(onColumnRename).toHaveBeenCalledWith({ + internalName, + displayName: rename, + }) + }) + }) + }) +}) diff --git a/ui/test/dashboards/components/GraphOptionsCustomizeColumns.test.tsx b/ui/test/dashboards/components/GraphOptionsCustomizeColumns.test.tsx new file mode 100644 index 0000000000..23dee525e4 --- /dev/null +++ b/ui/test/dashboards/components/GraphOptionsCustomizeColumns.test.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns' +import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn' +import {TIME_COLUMN_DEFAULT} from 'src/shared/constants/tableGraph' + +import {shallow} from 'enzyme' + +const setup = (override = {}) => { + const props = { + columns: [], + onColumnRename: () => {}, + ...override, + } + + const wrapper = shallow() + + return {wrapper, props} +} + +describe('Dashboards.Components.GraphOptionsCustomizeColumns', () => { + describe('rendering', () => { + it('displays label and all columns passed in', () => { + const columns = [TIME_COLUMN_DEFAULT] + const {wrapper} = setup({columns}) + const label = wrapper.find('label') + const customizableColumns = wrapper.find(GraphOptionsCustomizableColumn) + + expect(label.exists()).toBe(true) + expect(customizableColumns.exists()).toBe(true) + expect(customizableColumns.length).toBe(columns.length) + }) + }) +}) diff --git a/ui/test/dashboards/components/GraphOptionsTimeFormat.test.tsx b/ui/test/dashboards/components/GraphOptionsTimeFormat.test.tsx new file mode 100644 index 0000000000..840f47aacb --- /dev/null +++ b/ui/test/dashboards/components/GraphOptionsTimeFormat.test.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' +import {Dropdown} from 'src/shared/components/Dropdown' +import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip' +import InputClickToEdit from 'src/shared/components/InputClickToEdit' +import {TIME_FORMAT_CUSTOM} from 'src/shared/constants/tableGraph' +import {shallow} from 'enzyme' + +const setup = (override = {}) => { + const props = { + timeFormat: '', + onTimeFormatChange: () => {}, + ...override, + } + + return shallow() +} + +describe('Dashboards.Components.GraphOptionsTimeFormat', () => { + describe('rendering', () => { + describe('when it is not a custom format', () => { + it('renders only a dropdown', () => { + const wrapper = setup() + + expect(wrapper.find(Dropdown).exists()).toBe(true) + expect(wrapper.find(QuestionMarkTooltip).exists()).toBe(false) + expect(wrapper.find(InputClickToEdit).exists()).toBe(false) + }) + }) + + describe('when state custom format is true', () => { + it('renders all components', () => { + const wrapper = setup() + + wrapper.setState({customFormat: true}) + + expect(wrapper.find(Dropdown).exists()).toBe(true) + expect(wrapper.find(QuestionMarkTooltip).exists()).toBe(true) + expect(wrapper.find(InputClickToEdit).exists()).toBe(true) + }) + }) + + describe('when format is not from the dropdown options', () => { + it('renders all components with "custom" selected in dropdown', () => { + const timeFormat = 'mmmmmmm' + const wrapper = setup({timeFormat}) + const dropdown = wrapper.find(Dropdown) + const input = wrapper.find(InputClickToEdit) + + expect(dropdown.prop('selected')).toBe(TIME_FORMAT_CUSTOM) + expect(input.exists()).toBe(true) + expect(input.prop('value')).toBe(timeFormat) + }) + }) + }) + + describe('instance methods', () => { + describe('#handleChooseFormat', () => { + describe('when input is custom', () => { + it('sets the state custom format to true', () => { + const instance = setup().instance() as GraphOptionsTimeFormat + + instance.handleChooseFormat({text: TIME_FORMAT_CUSTOM}) + expect(instance.state.customFormat).toBe(true) + }) + }) + + describe('when input is not custom', () => { + it('sets the state custom format to false', () => { + const onTimeFormatChange = jest.fn() + const instance = setup({ + onTimeFormatChange, + }).instance() as GraphOptionsTimeFormat + + instance.handleChooseFormat({text: 'blah'}) + expect(instance.state.customFormat).toBe(false) + expect(onTimeFormatChange).toBeCalledWith('blah') + expect(onTimeFormatChange).toHaveBeenCalledTimes(1) + }) + }) + }) + }) +}) diff --git a/ui/test/dashboards/components/TableOptions.test.tsx b/ui/test/dashboards/components/TableOptions.test.tsx new file mode 100644 index 0000000000..c9242616c2 --- /dev/null +++ b/ui/test/dashboards/components/TableOptions.test.tsx @@ -0,0 +1,61 @@ +import React from 'react' + +import {TableOptions} from 'src/dashboards/components/TableOptions' + +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' +import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis' +import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy' +import GraphOptionsTextWrapping from 'src/dashboards/components/GraphOptionsTextWrapping' +import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns' +import ThresholdsList from 'src/shared/components/ThresholdsList' +import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle' + +import {shallow} from 'enzyme' + +const setup = (override = {}) => { + const props = { + queryConfigs: [], + handleUpdateTableOptions: () => {}, + tableOptions: { + timeFormat: '', + verticalTimeAxis: true, + sortBy: {internalName: '', displayName: ''}, + wrapping: '', + columnNames: [], + }, + onResetFocus: () => {}, + ...override, + } + + const wrapper = shallow() + + return {wrapper, props} +} + +describe('Dashboards.Components.TableOptions', () => { + describe('rendering', () => { + it('should render all components', () => { + const {wrapper} = setup() + const fancyScrollbar = wrapper.find(FancyScrollbar) + const graphOptionsTimeFormat = wrapper.find(GraphOptionsTimeFormat) + const graphOptionsTimeAxis = wrapper.find(GraphOptionsTimeAxis) + const graphOptionsSortBy = wrapper.find(GraphOptionsSortBy) + const graphOptionsTextWrapping = wrapper.find(GraphOptionsTextWrapping) + const graphOptionsCustomizeColumns = wrapper.find( + GraphOptionsCustomizeColumns + ) + const thresholdsList = wrapper.find(ThresholdsList) + const thresholdsListTypeToggle = wrapper.find(ThresholdsListTypeToggle) + + expect(fancyScrollbar.exists()).toBe(true) + expect(graphOptionsTimeFormat.exists()).toBe(true) + expect(graphOptionsTimeAxis.exists()).toBe(true) + expect(graphOptionsSortBy.exists()).toBe(true) + expect(graphOptionsTextWrapping.exists()).toBe(true) + expect(graphOptionsCustomizeColumns.exists()).toBe(true) + expect(thresholdsList.exists()).toBe(true) + expect(thresholdsListTypeToggle.exists()).toBe(true) + }) + }) +}) diff --git a/ui/test/dashboards/reducers/cellEditorOverlay.test.js b/ui/test/dashboards/reducers/cellEditorOverlay.test.js index 1cdd137e75..2d29ecf67b 100644 --- a/ui/test/dashboards/reducers/cellEditorOverlay.test.js +++ b/ui/test/dashboards/reducers/cellEditorOverlay.test.js @@ -5,17 +5,18 @@ import { hideCellEditorOverlay, changeCellType, renameCell, - updateSingleStatColors, - updateSingleStatType, + updateThresholdsListColors, + updateThresholdsListType, updateGaugeColors, updateAxes, } from 'src/dashboards/actions/cellEditorOverlay' +import {DEFAULT_TABLE_OPTIONS} from 'src/shared/constants/tableGraph' import { validateGaugeColors, - validateSingleStatColors, - getSingleStatType, -} from 'src/dashboards/constants/gaugeColors' + validateThresholdsListColors, + getThresholdsListType, +} from 'shared/constants/thresholds' const defaultCellType = 'line' const defaultCellName = 'defaultCell' @@ -35,12 +36,13 @@ const defaultCell = { colors: [], name: defaultCellName, type: defaultCellType, + tableOptions: DEFAULT_TABLE_OPTIONS, } -const defaultSingleStatType = getSingleStatType(defaultCell.colors) -const defaultSingleStatColors = validateSingleStatColors( +const defaultThresholdsListType = getThresholdsListType(defaultCell.colors) +const defaultThresholdsListColors = validateThresholdsListColors( defaultCell.colors, - defaultSingleStatType + defaultThresholdsListType ) const defaultGaugeColors = validateGaugeColors(defaultCell.colors) @@ -50,14 +52,15 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => { const expected = { cell: defaultCell, gaugeColors: defaultGaugeColors, - singleStatColors: defaultSingleStatColors, - singleStatType: defaultSingleStatType, + thresholdsListColors: defaultThresholdsListColors, + thresholdsListType: defaultThresholdsListType, + tableOptions: DEFAULT_TABLE_OPTIONS, } - expect(actual.cell).toBe(expected.cell) + expect(actual.cell).toEqual(expected.cell) expect(actual.gaugeColors).toBe(expected.gaugeColors) - expect(actual.singleStatColors).toBe(expected.singleStatColors) - expect(actual.singleStatType).toBe(expected.singleStatType) + expect(actual.thresholdsListColors).toBe(expected.thresholdsListColors) + expect(actual.thresholdsListType).toBe(expected.thresholdsListType) }) it('should hide cell editor overlay', () => { @@ -84,21 +87,21 @@ describe('Dashboards.Reducers.cellEditorOverlay', () => { it('should update the cell single stat colors', () => { const actual = reducer( initialState, - updateSingleStatColors(defaultSingleStatColors) + updateThresholdsListColors(defaultThresholdsListColors) ) - const expected = defaultSingleStatColors + const expected = defaultThresholdsListColors - expect(actual.singleStatColors).toBe(expected) + expect(actual.thresholdsListColors).toBe(expected) }) it('should toggle the single stat type', () => { const actual = reducer( initialState, - updateSingleStatType(defaultSingleStatType) + updateThresholdsListType(defaultThresholdsListType) ) - const expected = defaultSingleStatType + const expected = defaultThresholdsListType - expect(actual.singleStatType).toBe(expected) + expect(actual.thresholdsListType).toBe(expected) }) it('should update the cell gauge colors', () => { diff --git a/ui/test/kapacitor/containers/KapacitorPage.test.tsx b/ui/test/kapacitor/containers/KapacitorPage.test.tsx index 9cd879ee9f..0fe9717476 100644 --- a/ui/test/kapacitor/containers/KapacitorPage.test.tsx +++ b/ui/test/kapacitor/containers/KapacitorPage.test.tsx @@ -17,7 +17,7 @@ jest.mock('src/shared/apis', () => require('mocks/shared/apis')) const setup = (override = {}) => { const props = { source: source, - addFlashMessage: () => {}, + notify: () => {}, kapacitor, router: { push: () => {}, @@ -57,19 +57,53 @@ describe('Kapacitor.Containers.KapacitorPage', () => { describe('user interactions ', () => { describe('entering the url', () => { - it('renders the text that is inputted', () => { - const {wrapper} = setup() - const value = '/new/url' - const event = {target: {value}} + describe('with a http url', () => { + it('renders the text that is inputted', () => { + const {wrapper} = setup() + const value = 'http://example.com' + const event = {target: {value}} - let inputElement = wrapper.find('#kapaUrl') + let inputElement = wrapper.find('#kapaUrl') - inputElement.simulate('change', event) - wrapper.update() + inputElement.simulate('change', event) - inputElement = wrapper.find('#kapaUrl') + inputElement = wrapper.find('#kapaUrl') + const secureCheckbox = wrapper.find('#insecureSkipVerifyCheckbox') - expect(inputElement.prop('value')).toBe(value) + expect(secureCheckbox.exists()).toBe(false) + expect(inputElement.prop('value')).toBe(value) + }) + }) + + describe('with a https url', () => { + let inputElement, secureCheckbox, wrapper + const value = 'https://example.com' + + beforeEach(() => { + wrapper = setup().wrapper + const event = {target: {value}} + + inputElement = wrapper.find('#kapaUrl') + inputElement.simulate('change', event) + inputElement = wrapper.find('#kapaUrl') + secureCheckbox = wrapper.find('#insecureSkipVerifyCheckbox') + }) + + describe('checking the insecure skip verify checkbox', () => { + it("changes the state", () => { + const checked = true + const event = {target: {checked}} + + secureCheckbox.simulate('change', event) + secureCheckbox = wrapper.find('#insecureSkipVerifyCheckbox') + expect(secureCheckbox.prop('checked')).toBe(true) + }) + }) + + it('renders the https secure checkbox', () => { + expect(secureCheckbox.exists()).toBe(true) + expect(inputElement.prop('value')).toBe(value) + }) }) }) @@ -199,6 +233,7 @@ describe('Kapacitor.Containers.KapacitorPage', () => { await wrapper.instance().componentDidMount() expect(wrapper.state().kapacitor).toEqual(mocks.kapacitor) + expect(wrapper.state().exists).toBe(true) }) }) }) diff --git a/ui/test/resources.ts b/ui/test/resources.ts index 6e8c873bb2..5bee400466 100644 --- a/ui/test/resources.ts +++ b/ui/test/resources.ts @@ -1,3 +1,14 @@ +export const links = { + self: '/chronograf/v1/sources/16', + kapacitors: '/chronograf/v1/sources/16/kapacitors', + proxy: '/chronograf/v1/sources/16/proxy', + queries: '/chronograf/v1/sources/16/queries', + write: '/chronograf/v1/sources/16/write', + permissions: '/chronograf/v1/sources/16/permissions', + users: '/chronograf/v1/sources/16/users', + databases: '/chronograf/v1/sources/16/dbs', +} + export const source = { id: '16', name: 'ssl', @@ -9,16 +20,7 @@ export const source = { telegraf: 'telegraf', organization: '0', role: 'viewer', - links: { - self: '/chronograf/v1/sources/16', - kapacitors: '/chronograf/v1/sources/16/kapacitors', - proxy: '/chronograf/v1/sources/16/proxy', - queries: '/chronograf/v1/sources/16/queries', - write: '/chronograf/v1/sources/16/write', - permissions: '/chronograf/v1/sources/16/permissions', - users: '/chronograf/v1/sources/16/users', - databases: '/chronograf/v1/sources/16/dbs', - }, + links, } export const query = { @@ -54,6 +56,7 @@ export const kapacitor = { username: 'influx', password: '', active: false, + insecureSkipVerify: false, links: { self: '/kapa/1', proxy: '/proxy/kapacitor/1', @@ -332,3 +335,32 @@ export const kapacitorRules = [ }, }, ] + +export const authLinks = { + allUsers: '/chronograf/v1/users', + auth: [ + { + callback: '/oauth/github/callback', + label: 'Github', + login: '/oauth/github/login', + logout: '/oauth/github/logout', + name: 'github', + }, + ], + config: { + auth: '/chronograf/v1/config/auth', + self: '/chronograf/v1/config', + }, + dashboards: '/chronograf/v1/dashboards', + environment: '/chronograf/v1/env', + external: { + statusFeed: 'https://www.influxdata.com/feed/json', + }, + layouts: '/chronograf/v1/layouts', + logout: '/oauth/logout', + mappings: '/chronograf/v1/mappings', + me: '/chronograf/v1/me', + organizations: '/chronograf/v1/organizations', + sources: '/chronograf/v1/sources', + users: '/chronograf/v1/organizations/default/users', +} diff --git a/ui/test/shared/apis/index.test.ts b/ui/test/shared/apis/index.test.ts new file mode 100644 index 0000000000..482b8572fa --- /dev/null +++ b/ui/test/shared/apis/index.test.ts @@ -0,0 +1,42 @@ +import {createKapacitor, updateKapacitor} from 'src/shared/apis' +import { + source, + kapacitor, + updateKapacitorBody, + createKapacitorBody, +} from 'mocks/dummy' +import AJAX from 'src/utils/ajax' + +jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) + +describe('Shared.Apis', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('createKapacitor', () => { + it('is called with the expected body', () => { + createKapacitor(source, createKapacitorBody) + + expect(AJAX).toHaveBeenCalledWith({ + url: source.links.kapacitors, + method: 'POST', + data: createKapacitorBody, + }) + }) + }) + + describe('updateKapacitor', () => { + it('is called with the expected body', () => { + updateKapacitor(updateKapacitorBody) + let data = {...updateKapacitorBody} + delete data.links + + expect(AJAX).toHaveBeenCalledWith({ + url: kapacitor.links.self, + method: 'PATCH', + data, + }) + }) + }) +}) diff --git a/ui/test/shared/reducers/notifications.test.js b/ui/test/shared/reducers/notifications.test.js new file mode 100644 index 0000000000..4c57534d44 --- /dev/null +++ b/ui/test/shared/reducers/notifications.test.js @@ -0,0 +1,58 @@ +import {initialState, notifications} from 'shared/reducers/notifications' + +import {notify, dismissNotification} from 'shared/actions/notifications' + +import {FIVE_SECONDS} from 'shared/constants/index' + +const notificationID = '000' + +const exampleNotification = { + id: notificationID, + type: 'success', + message: 'Hell yeah you are a real notification!', + duration: FIVE_SECONDS, + icon: 'zap', +} + +const exampleNotifications = [exampleNotification] + +describe('Shared.Reducers.notifications', () => { + it('should publish a notification', () => { + const [actual] = notifications(initialState, notify(exampleNotification)) + + const [expected] = [exampleNotification, ...initialState] + + expect(actual.type).toEqual(expected.type) + expect(actual.icon).toEqual(expected.icon) + expect(actual.message).toEqual(expected.message) + expect(actual.duration).toEqual(expected.duration) + }) + + describe('adding more than one notification', () => { + it('should put the new notification at the beggining of the list', () => { + const newNotification = { + type: 'error', + message: 'new notification', + duration: FIVE_SECONDS, + icon: 'zap', + } + + const actual = notifications( + exampleNotifications, + notify(newNotification) + ) + + expect(actual.length).toBe(2) + expect(actual[0].message).toEqual(newNotification.message) + }) + }) + + it('should dismiss a notification', () => { + const actual = notifications( + exampleNotifications, + dismissNotification(notificationID) + ) + + expect(actual.length).toBe(0) + }) +}) diff --git a/ui/yarn.lock b/ui/yarn.lock index 80fc640763..c46007e8d9 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8531,12 +8531,6 @@ upath@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" -updeep@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/updeep/-/updeep-0.13.0.tgz#8fe46d3802711b7f265f7cc6c03f21b240e9d632" - dependencies: - lodash "^4.2.0" - upper-case@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"