Merge branch 'master' into bugfix/task_enabled_correct_check

pull/10616/head
Jared Scheib 2018-03-16 21:03:58 -07:00 committed by Jared Scheib
commit 9e9f566996
127 changed files with 3515 additions and 1968 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ chronograf*.db
*_gen.go
canned/apps_gen.go
npm-debug.log
yarn-error.log

View File

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

View File

@ -101,7 +101,7 @@ gotestrace:
go test -race ./...
jstest:
cd ui && yarn test
cd ui && yarn test --runInBand
run: ${BINARY}
./chronograf

View File

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

View File

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

View File

@ -37,6 +37,20 @@ message DashboardCell {
map<string, Axis> 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 {

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@
]
}
],
"colors": [],
"type": "single-stat"
},
{
@ -73,6 +74,7 @@
]
}
],
"colors": [],
"type": "single-stat"
},
{

View File

@ -117,6 +117,7 @@
"h": 4,
"i": "0fa47984-825b-46f1-9ca5-0366e3220008",
"name": "Mesos Master Uptime",
"colors": [],
"type": "single-stat",
"queries": [
{

View File

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

View File

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

View File

@ -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"}}
`,
},
{

View File

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

View File

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

View File

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

View File

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

1
ui/mocks/utils/ajax.ts Normal file
View File

@ -0,0 +1 @@
export default jest.fn(() => Promise.resolve())

View File

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

View File

@ -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}) =>
<div className="chronograf-root">
<Notifications />
<SideNav />
{children}
</div>
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 (
<div className="chronograf-root">
<Notifications />
<SideNav />
{this.props.children &&
React.cloneElement(this.props.children, {
addFlashMessage: this.handleAddFlashMessage,
})}
</div>
)
},
})
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(publishNotification, dispatch),
})
export default connect(null, mapDispatchToProps)(App)
export default App

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import DatabaseTable from 'src/admin/components/DatabaseTable'
const DatabaseManager = ({
databases,
notify,
isRFDisplayed,
isAddDBDisabled,
addDatabase,
@ -46,7 +45,6 @@ const DatabaseManager = ({
<DatabaseTable
key={db.links.self}
database={db}
notify={notify}
isRFDisplayed={isRFDisplayed}
onEditDatabase={onEditDatabase}
onKeyDownDatabase={onKeyDownDatabase}
@ -74,7 +72,6 @@ const {arrayOf, bool, func, shape} = PropTypes
DatabaseManager.propTypes = {
databases: arrayOf(shape()),
notify: func,
addDatabase: func,
isRFDisplayed: bool,
isAddDBDisabled: bool,

View File

@ -1,10 +1,16 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import onClickOutside from 'react-onclickoutside'
import {notify as notifyAction} from 'shared/actions/notifications'
import {formatRPDuration} from 'utils/formatting'
import YesNoButtons from 'shared/components/YesNoButtons'
import {DATABASE_TABLE} from 'src/admin/constants/tableSizing'
import {NOTIFY_RETENTION_POLICY_CANT_HAVE_EMPTY_FIELDS} from 'shared/copy/notifications'
class DatabaseRow extends Component {
constructor(props) {
@ -110,7 +116,7 @@ class DatabaseRow extends Component {
const replication = isRFDisplayed ? +this.replication.value.trim() : 1
if (!duration || (isRFDisplayed && !replication)) {
notify('error', 'Fields cannot be empty')
notify(NOTIFY_RETENTION_POLICY_CANT_HAVE_EMPTY_FIELDS)
return
}
@ -261,8 +267,12 @@ DatabaseRow.propTypes = {
onCreate: func,
onUpdate: func,
onDelete: func,
notify: func,
notify: func.isRequired,
isRFDisplayed: bool,
}
export default onClickOutside(DatabaseRow)
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(onClickOutside(DatabaseRow))

View File

@ -12,7 +12,6 @@ const {func, shape, bool} = PropTypes
const DatabaseTable = ({
database,
notify,
isRFDisplayed,
onEditDatabase,
onKeyDownDatabase,
@ -36,7 +35,6 @@ const DatabaseTable = ({
>
<DatabaseTableHeader
database={database}
notify={notify}
onEdit={onEditDatabase}
onCancel={onCancelDatabase}
onDelete={onDeleteDatabase}
@ -74,7 +72,6 @@ const DatabaseTable = ({
return (
<DatabaseRow
key={rp.links.self}
notify={notify}
database={database}
retentionPolicy={rp}
onCreate={onCreateRetentionPolicy}
@ -96,7 +93,6 @@ const DatabaseTable = ({
DatabaseTable.propTypes = {
onEditDatabase: func,
database: shape(),
notify: func,
isRFDisplayed: bool,
isAddRPDisabled: bool,
onKeyDownDatabase: func,

View File

@ -1,7 +1,12 @@
import React 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 ConfirmButtons from 'shared/components/ConfirmButtons'
import {NOTIFY_DATABASE_DELETE_CONFIRMATION_REQUIRED} from 'shared/copy/notifications'
const DatabaseTableHeader = ({
database,
@ -76,7 +81,7 @@ const Header = ({
const onConfirm = db => {
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)

View File

@ -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}
</tbody>
@ -209,7 +212,6 @@ AllUsersTable.propTypes = {
superAdminNewUsers: bool,
}),
meID: string.isRequired,
notify: func.isRequired,
isLoading: bool.isRequired,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Props, State> {
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 (
<AllUsersTable
meID={meID}
users={users}
links={links}
notify={notify}
authConfig={authConfig}
actionsConfig={actionsConfig}
organizations={organizations}
isLoading={this.state.isLoading}
onDeleteUser={this.handleDeleteUser}
onCreateUser={this.handleCreateUser}
onUpdateUserRoles={this.handleUpdateUserRoles}
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
onDeleteUser={this.handleDeleteUser}
links={links}
authConfig={authConfig}
actionsConfig={actionsConfig}
notify={notify}
isLoading={this.state.isLoading}
/>
)
}
}
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)

View File

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

View File

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

View File

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

View File

@ -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 {
<Tabber
labelText="Y-Value's Format"
tipID="Y-Values's Format"
tipContent={TOOLTIP_CONTENT.FORMAT}
tipContent={TOOLTIP_Y_VALUE_FORMAT}
>
<Tab
text="K/M/B"

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import _ from 'lodash'
import uuid from 'uuid'
import ResizeContainer from 'shared/components/ResizeContainer'
import ResizeContainer from 'src/shared/components/ResizeContainer'
import QueryMaker from 'src/dashboards/components/QueryMaker'
import Visualization from 'src/dashboards/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
@ -21,10 +21,10 @@ import {
removeUnselectedTemplateValues,
TYPE_QUERY_CONFIG,
} from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {AUTO_GROUP_BY} from 'shared/constants'
import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors'
import {AUTO_GROUP_BY} from 'src/shared/constants'
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
class CellEditorOverlay extends Component {
constructor(props) {
@ -106,7 +106,7 @@ class CellEditorOverlay extends Component {
handleSaveCell = () => {
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,
}

View File

@ -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 <SingleStatOptions onResetFocus={onResetFocus} />
case 'table':
return <TableOptions />
return (
<TableOptions
onResetFocus={onResetFocus}
queryConfigs={queryConfigs}
/>
)
default:
return (
<AxesOptions

View File

@ -11,10 +11,10 @@ import Threshold from 'src/dashboards/components/Threshold'
import {
COLOR_TYPE_THRESHOLD,
GAUGE_COLORS,
THRESHOLD_COLORS,
MAX_THRESHOLDS,
MIN_THRESHOLDS,
} from 'src/dashboards/constants/gaugeColors'
} from 'shared/constants/thresholds'
import {
updateGaugeColors,
@ -27,7 +27,7 @@ class GaugeOptions extends Component {
const sortedColors = _.sortBy(gaugeColors, color => 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 {
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Gauge Controls</h5>
<div className="gauge-controls">
<div className="thresholds-list">
<button
className="btn btn-sm btn-primary gauge-controls--add-threshold"
className="btn btn-sm btn-primary"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>

View File

@ -1,34 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputClickToEdit from 'shared/components/InputClickToEdit'
const GraphOptionsCustomizableColumn = ({
originalColumnName,
newColumnName,
onColumnRename,
}) => {
return (
<div className="column-controls--section">
<div className="column-controls--label">
{originalColumnName}
</div>
<InputClickToEdit
value={newColumnName}
wrapperClass="column-controls-input"
onBlur={onColumnRename}
placeholder="Rename..."
appearAsNormalInput={true}
/>
</div>
)
}
const {func, string} = PropTypes
GraphOptionsCustomizableColumn.propTypes = {
originalColumnName: string,
newColumnName: string,
onColumnRename: func,
}
export default GraphOptionsCustomizableColumn

View File

@ -0,0 +1,48 @@
import React, {PureComponent} from 'react'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
type Column = {
internalName: string
displayName: string
}
interface Props {
internalName: string
displayName: string
onColumnRename: (column: Column) => void
}
class GraphOptionsCustomizableColumn extends PureComponent<Props, {}> {
constructor(props) {
super(props)
this.handleColumnRename = this.handleColumnRename.bind(this)
}
handleColumnRename(rename) {
const {onColumnRename, internalName} = this.props
onColumnRename({internalName, displayName: rename})
}
render() {
const {internalName, displayName} = this.props
return (
<div className="column-controls--section">
<div className="column-controls--label">
{internalName}
</div>
<InputClickToEdit
value={displayName}
wrapperClass="column-controls-input"
onBlur={this.handleColumnRename}
placeholder="Rename..."
appearAsNormalInput={true}
/>
</div>
)
}
}
export default GraphOptionsCustomizableColumn

View File

@ -1,10 +1,22 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {SFC} from 'react'
import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn'
import uuid from 'uuid'
const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
type Column = {
internalName: string
displayName: string
}
interface Props {
columns: Column[]
onColumnRename: (column: Column) => void
}
const GraphOptionsCustomizeColumns: SFC<Props> = ({
columns,
onColumnRename,
}) => {
return (
<div className="graph-options-group">
<label className="form-label">Customize Columns</label>
@ -12,8 +24,8 @@ const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
return (
<GraphOptionsCustomizableColumn
key={uuid.v4()}
originalColumnName={col.name}
newColumnName={col.newName}
internalName={col.internalName}
displayName={col.displayName}
onColumnRename={onColumnRename}
/>
)
@ -21,16 +33,5 @@ const GraphOptionsCustomizeColumns = ({columns, onColumnRename}) => {
</div>
)
}
const {arrayOf, func, shape, string} = PropTypes
GraphOptionsCustomizeColumns.propTypes = {
columns: arrayOf(
shape({
name: string,
newName: string,
})
),
onColumnRename: func,
}
export default GraphOptionsCustomizeColumns

View File

@ -2,30 +2,39 @@ import React from 'react'
import PropTypes from 'prop-types'
import {
SINGLE_STAT_BG,
SINGLE_STAT_TEXT,
} from 'src/dashboards/constants/gaugeColors'
THRESHOLD_TYPE_BG,
THRESHOLD_TYPE_TEXT,
} from 'shared/constants/thresholds'
// TODO: Needs major refactoring to make thresholds a much more general component to be shared between single stat, gauge, and table.
const GraphOptionsTextWrapping = ({singleStatType, onToggleTextWrapping}) => {
const GraphOptionsTextWrapping = ({
thresholdsListType,
onToggleTextWrapping,
}) => {
return (
<div className="form-group col-xs-12">
<label>Text Wrapping</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${singleStatType === SINGLE_STAT_BG ? 'active' : ''}`}
className={`${thresholdsListType === THRESHOLD_TYPE_BG
? 'active'
: ''}`}
onClick={onToggleTextWrapping}
>
Truncate
</li>
<li
className={`${singleStatType === SINGLE_STAT_TEXT ? 'active' : ''}`}
className={`${thresholdsListType === THRESHOLD_TYPE_TEXT
? 'active'
: ''}`}
onClick={onToggleTextWrapping}
>
Wrap
</li>
<li
className={`${singleStatType === SINGLE_STAT_BG ? 'active' : ''}`}
className={`${thresholdsListType === THRESHOLD_TYPE_BG
? 'active'
: ''}`}
onClick={onToggleTextWrapping}
>
Single Line
@ -37,7 +46,7 @@ const GraphOptionsTextWrapping = ({singleStatType, onToggleTextWrapping}) => {
const {func, string} = PropTypes
GraphOptionsTextWrapping.propTypes = {
singleStatType: string,
thresholdsListType: string,
onToggleTextWrapping: func,
}

View File

@ -1,41 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
SINGLE_STAT_BG,
SINGLE_STAT_TEXT,
} from 'src/dashboards/constants/gaugeColors'
// TODO: Needs major refactoring to make thresholds a much more general component to be shared between single stat, gauge, and table.
const GraphOptionsThresholdColoring = ({
onToggleSingleStatType,
singleStatType,
}) => {
return (
<div className="form-group col-xs-12 col-md-6">
<label>Threshold Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${singleStatType === SINGLE_STAT_BG ? 'active' : ''}`}
onClick={onToggleSingleStatType(SINGLE_STAT_BG)}
>
Background
</li>
<li
className={`${singleStatType === SINGLE_STAT_TEXT ? 'active' : ''}`}
onClick={onToggleSingleStatType(SINGLE_STAT_TEXT)}
>
Text
</li>
</ul>
</div>
)
}
const {func, string} = PropTypes
GraphOptionsThresholdColoring.propTypes = {
singleStatType: string,
onToggleSingleStatType: func,
}
export default GraphOptionsThresholdColoring

View File

@ -1,78 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Threshold from 'src/dashboards/components/Threshold'
import ColorDropdown from 'shared/components/ColorDropdown'
import {
GAUGE_COLORS,
SINGLE_STAT_BASE,
} from 'src/dashboards/constants/gaugeColors'
// TODO: Needs major refactoring to make thresholds a much more general component to be shared between single stat, gauge, and table.
const GraphOptionsThresholds = ({
onAddThreshold,
disableAddThreshold,
sortedColors,
formatColor,
onChooseColor,
onValidateColorValue,
onUpdateColorValue,
onDeleteThreshold,
}) => {
return (
<div className="gauge-controls graph-options-group">
<label className="form-label">Thresholds</label>
<button
className="btn btn-sm btn-primary gauge-controls--add-threshold"
onClick={onAddThreshold}
disabled={disableAddThreshold}
>
<span className="icon plus" /> Add Threshold
</button>
{sortedColors.map(
color =>
color.id === SINGLE_STAT_BASE
? <div className="gauge-controls--section" key={color.id}>
<div className="gauge-controls--label">Base Color</div>
<ColorDropdown
colors={GAUGE_COLORS}
selected={formatColor(color)}
onChoose={onChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={onChooseColor}
onValidateColorValue={onValidateColorValue}
onUpdateColorValue={onUpdateColorValue}
onDeleteThreshold={onDeleteThreshold}
/>
)}
</div>
)
}
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

View File

@ -1,22 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const GraphOptionsTimeFormat = ({TimeFormat, onTimeFormatChange}) =>
<div className="form-group col-xs-12">
<label>Time Format</label>
<input
className="form-control input-sm"
placeholder="mm/dd/yyyy HH:mm:ss.ss"
value={TimeFormat}
onChange={onTimeFormatChange}
/>
</div>
const {func, string} = PropTypes
GraphOptionsTimeFormat.propTypes = {
TimeFormat: string,
onTimeFormatChange: func,
}
export default GraphOptionsTimeFormat

View File

@ -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<Props, State> {
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 (
<div className="form-group col-xs-12">
<label>
Time Format
{customFormat &&
<QuestionMarkTooltip
tipID="Time Axis Format"
tipContent={tipContent}
/>}
</label>
<Dropdown
items={FORMAT_OPTIONS}
selected={showCustom ? TIME_FORMAT_CUSTOM : format}
buttonColor="btn-default"
buttonSize="btn-xs"
className="dropdown-stretch"
onChoose={this.handleChooseFormat}
/>
{showCustom &&
<div className="column-controls--section">
<InputClickToEdit
wrapperClass="column-controls-input "
value={format}
onBlur={onTimeFormatChange}
onChange={onTimeFormatChange}
placeholder="Enter custom format..."
appearAsNormalInput={true}
/>
</div>}
</div>
)
}
}
export default GraphOptionsTimeFormat

View File

@ -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 (
<FancyScrollbar
@ -155,38 +34,7 @@ class SingleStatOptions extends Component {
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Single Stat Controls</h5>
<div className="gauge-controls">
<button
className="btn btn-sm btn-primary gauge-controls--add-threshold"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>
<span className="icon plus" /> Add Threshold
</button>
{singleStatColors.map(
color =>
color.id === SINGLE_STAT_BASE
? <div className="gauge-controls--section" key={color.id}>
<div className="gauge-controls--label">Base Color</div>
<ColorDropdown
colors={GAUGE_COLORS}
selected={formatColor(color)}
onChoose={this.handleChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onDeleteThreshold={this.handleDeleteThreshold}
onSortColors={this.handleSortColors}
/>
)}
</div>
<ThresholdsList onResetFocus={onResetFocus} />
<div className="graph-options-group form-group-wrapper">
<div className="form-group col-xs-6">
<label>Prefix</label>
@ -208,27 +56,7 @@ class SingleStatOptions extends Component {
maxLength="5"
/>
</div>
<div className="form-group col-xs-6">
<label>Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${singleStatType === SINGLE_STAT_BG
? 'active'
: ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_BG)}
>
Background
</li>
<li
className={`${singleStatType === SINGLE_STAT_TEXT
? 'active'
: ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_TEXT)}
>
Text
</li>
</ul>
</div>
<ThresholdsListTypeToggle containerClass="form-group col-xs-6" />
</div>
</div>
</FancyScrollbar>
@ -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),
})

View File

@ -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 (
<FancyScrollbar
className="display-options--cell y-axis-controls"
autoHide={false}
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Table Controls</h5>
<div className="form-group-wrapper">
<GraphOptionsTimeFormat
TimeFormat={TimeFormat}
onTimeFormatChange={this.handleTimeFormatChange}
/>
<GraphOptionsTimeAxis
TimeAxis={TimeAxis}
onToggleTimeAxis={this.handleToggleTimeAxis}
/>
<GraphOptionsSortBy
sortByOptions={tableSortByOptions}
onChooseSortBy={this.handleChooseSortBy}
/>
<GraphOptionsTextWrapping
singleStatType={singleStatType}
onToggleTextWrapping={this.handleToggleTextWrapping}
/>
</div>
<GraphOptionsCustomizeColumns
columns={columns}
onColumnRename={this.handleColumnRename}
/>
<GraphOptionsThresholds
onAddThreshold={this.handleAddThreshold}
disableAddThreshold={disableAddThreshold}
sortedColors={sortedColors}
formatColor={formatColor}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onDeleteThreshold={this.handleDeleteThreshold}
/>
<div className="form-group-wrapper graph-options-group">
<GraphOptionsThresholdColoring
onToggleSingleStatType={this.handleToggleSingleStatType}
singleStatColors={singleStatType}
/>
</div>
</div>
</FancyScrollbar>
)
}
}
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)

View File

@ -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<Props, {}> {
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 (
<FancyScrollbar
className="display-options--cell y-axis-controls"
autoHide={false}
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Table Controls</h5>
<div className="form-group-wrapper">
<GraphOptionsTimeFormat
timeFormat={timeFormat}
onTimeFormatChange={this.handleTimeFormatChange}
/>
<GraphOptionsTimeAxis
TimeAxis={TimeAxis}
onToggleTimeAxis={this.handleToggleTimeAxis}
/>
<GraphOptionsSortBy
sortByOptions={tableSortByOptions}
onChooseSortBy={this.handleChooseSortBy}
/>
<GraphOptionsTextWrapping
thresholdsListType="background"
onToggleTextWrapping={this.handleToggleTextWrapping}
/>
</div>
<GraphOptionsCustomizeColumns
columns={columns}
onColumnRename={this.handleColumnRename}
/>
<ThresholdsList showListHeading={true} onResetFocus={onResetFocus} />
<div className="form-group-wrapper graph-options-group">
<ThresholdsListTypeToggle containerClass="form-group col-xs-6" />
</div>
</div>
</FancyScrollbar>
)
}
}
const mapStateToProps = ({cellEditorOverlay: {cell: {tableOptions}}}) => ({
tableOptions,
})
const mapDispatchToProps = dispatch => ({
handleUpdateTableOptions: bindActionCreators(updateTableOptions, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(TableOptions)

View File

@ -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 (
<div className="gauge-controls--section">
<div className="threshold-item">
<div className={labelClass}>
{label}
</div>
{canBeDeleted
? <button
className="btn btn-default btn-sm btn-square gauge-controls--delete"
className="btn btn-default btn-sm btn-square"
onClick={onDeleteThreshold(threshold)}
>
<span className="icon remove" />
@ -99,7 +99,7 @@ class Threshold extends Component {
ref={r => (this.thresholdInputRef = r)}
/>
<ColorDropdown
colors={GAUGE_COLORS}
colors={THRESHOLD_COLORS}
selected={selectedColor}
onChoose={onChooseColor(threshold)}
disabled={isMax && disableMaxColor}

View File

@ -2,11 +2,11 @@ import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import buildQueries from 'utils/buildQueriesForGraphs'
import VisualizationName from 'src/dashboards/components/VisualizationName'
import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors'
import {stringifyColorValues} from 'src/shared/constants/colorOperations'
const DashVisualization = (
{
@ -20,11 +20,12 @@ const DashVisualization = (
editQueryStatus,
resizerTopHeight,
staticLegend,
singleStatColors,
thresholdsListColors,
tableOptions,
},
{source: {links: {proxy}}}
) => {
const colors = type === 'gauge' ? gaugeColors : singleStatColors
const colors = type === 'gauge' ? gaugeColors : thresholdsListColors
return (
<div className="graph">
@ -34,6 +35,7 @@ const DashVisualization = (
colors={stringifyColorValues(colors)}
axes={axes}
type={type}
tableOptions={tableOptions}
queries={buildQueries(proxy, queryConfigs, timeRange)}
templates={templates}
autoRefresh={autoRefresh}
@ -63,8 +65,9 @@ DashVisualization.propTypes = {
bounds: arrayOf(string),
}),
}),
tableOptions: shape({}),
resizerTopHeight: number,
singleStatColors: arrayOf(
thresholdsListColors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
@ -94,12 +97,17 @@ DashVisualization.contextTypes = {
}
const mapStateToProps = ({
cellEditorOverlay: {singleStatColors, gaugeColors, cell: {type, axes}},
cellEditorOverlay: {
thresholdsListColors,
gaugeColors,
cell: {type, axes, tableOptions},
},
}) => ({
gaugeColors,
singleStatColors,
thresholdsListColors,
type,
axes,
tableOptions,
})
export default connect(mapStateToProps, null)(DashVisualization)

View File

@ -22,7 +22,9 @@ import {TEMPLATE_TYPES} from 'src/dashboards/constants'
import generateTemplateVariableQuery from 'src/dashboards/utils/templateVariableQueryGenerator'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {notify as notifyAction} from 'shared/actions/notifications'
import {NOTIFY_TEMP_VAR_ALREADY_EXISTS} from 'shared/copy/notifications'
const compact = values => uniq(values).filter(value => /\S/.test(value))
@ -144,10 +146,7 @@ class RowWrapper extends Component {
const tempVar = `\u003a${_tempVar}\u003a` // add ':'s
if (tempVarAlreadyExists(tempVar, id)) {
return notify(
'error',
`Variable '${_tempVar}' already exists. Please enter a new value.`
)
return notify(NOTIFY_TEMP_VAR_ALREADY_EXISTS(_tempVar))
}
this.setState({
@ -348,7 +347,7 @@ TemplateVariableRow.propTypes = {
const mapDispatchToProps = dispatch => ({
onErrorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(OnClickOutside(RowWrapper))

View File

@ -0,0 +1,20 @@
import {DEFAULT_TABLE_OPTIONS} from 'src/shared/constants/tableGraph'
export const initializeOptions = cellType => {
switch (cellType) {
case 'table':
return DEFAULT_TABLE_OPTIONS
default:
return {}
}
}
export const AXES_SCALE_OPTIONS = {
LINEAR: 'linear',
LOG: 'log',
BASE_2: '2',
BASE_10: '10',
}
export const TOOLTIP_Y_VALUE_FORMAT =
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>'

View File

@ -91,16 +91,7 @@ export const removeUnselectedTemplateValues = templates => {
return {...template, values: selectedValues}
})
}
export const DISPLAY_OPTIONS = {
LINEAR: 'linear',
LOG: 'log',
BASE_2: '2',
BASE_10: '10',
}
export const TOOLTIP_CONTENT = {
FORMAT:
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
}
export const TYPE_QUERY_CONFIG = 'queryConfig'
export const TYPE_SHIFTED = 'shifted queryConfig'
export const TYPE_IFQL = 'ifql'

View File

@ -16,7 +16,7 @@ import TemplateVariableManager from 'src/dashboards/components/template_variable
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {publishNotification} from 'shared/actions/notifications'
import {notify as notifyAction} from 'shared/actions/notifications'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph'
@ -34,6 +34,7 @@ import {
} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
import {NOTIFY_DASHBOARD_NOT_FOUND} from 'shared/copy/notifications'
const FORMAT_INFLUXQL = 'influxql'
const defaultTimeRange = {
@ -90,7 +91,7 @@ class DashboardPage extends Component {
if (!dashboard) {
router.push(`/sources/${source.id}/dashboards`)
return notify('error', `Dashboard ${dashboardID} could not be found`)
return notify(NOTIFY_DASHBOARD_NOT_FOUND(dashboardID))
}
// Refresh and persists influxql generated template variable values.
@ -272,8 +273,8 @@ class DashboardPage extends Component {
manualRefresh,
onManualRefresh,
cellQueryStatus,
singleStatType,
singleStatColors,
thresholdsListType,
thresholdsListColors,
dashboardActions,
inPresentationMode,
handleChooseAutoRefresh,
@ -379,8 +380,8 @@ class DashboardPage extends Component {
onCancel={handleHideCellEditorOverlay}
templates={templatesIncludingDashTime}
editQueryStatus={dashboardActions.editCellQueryStatus}
singleStatType={singleStatType}
singleStatColors={singleStatColors}
thresholdsListType={thresholdsListType}
thresholdsListColors={thresholdsListColors}
gaugeColors={gaugeColors}
/>
: null}
@ -511,8 +512,8 @@ DashboardPage.propTypes = {
handleShowCellEditorOverlay: func.isRequired,
handleHideCellEditorOverlay: func.isRequired,
selectedCell: shape({}),
singleStatType: string.isRequired,
singleStatColors: arrayOf(shape({}).isRequired).isRequired,
thresholdsListType: string.isRequired,
thresholdsListColors: arrayOf(shape({}).isRequired).isRequired,
gaugeColors: arrayOf(shape({}).isRequired).isRequired,
}
@ -526,7 +527,12 @@ const mapStateToProps = (state, {params: {dashboardID}}) => {
sources,
dashTimeV1,
auth: {me, isUsingAuth},
cellEditorOverlay: {cell, singleStatType, singleStatColors, gaugeColors},
cellEditorOverlay: {
cell,
thresholdsListType,
thresholdsListColors,
gaugeColors,
},
} = state
const meRole = _.get(me, 'role', null)
@ -553,8 +559,8 @@ const mapStateToProps = (state, {params: {dashboardID}}) => {
inPresentationMode,
showTemplateControlBar,
selectedCell,
singleStatType,
singleStatColors,
thresholdsListType,
thresholdsListColors,
gaugeColors,
}
}
@ -568,7 +574,7 @@ const mapDispatchToProps = dispatch => ({
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(publishNotification, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
getAnnotationsAsync: bindActionCreators(
annotationActions.getAnnotationsAsync,
dispatch

View File

@ -1,16 +1,18 @@
import {
SINGLE_STAT_TEXT,
DEFAULT_SINGLESTAT_COLORS,
THRESHOLD_TYPE_TEXT,
DEFAULT_THRESHOLDS_LIST_COLORS,
DEFAULT_GAUGE_COLORS,
validateGaugeColors,
validateSingleStatColors,
getSingleStatType,
} from 'src/dashboards/constants/gaugeColors'
validateThresholdsListColors,
getThresholdsListType,
} from 'shared/constants/thresholds'
import {initializeOptions} from 'src/dashboards/constants/cellEditor'
export const initialState = {
cell: null,
singleStatType: SINGLE_STAT_TEXT,
singleStatColors: DEFAULT_SINGLESTAT_COLORS,
thresholdsListType: THRESHOLD_TYPE_TEXT,
thresholdsListColors: DEFAULT_THRESHOLDS_LIST_COLORS,
gaugeColors: DEFAULT_GAUGE_COLORS,
}
@ -19,11 +21,22 @@ export default function cellEditorOverlay(state = initialState, action) {
case 'SHOW_CELL_EDITOR_OVERLAY': {
const {cell, cell: {colors}} = action.payload
const singleStatType = getSingleStatType(colors)
const singleStatColors = validateSingleStatColors(colors, singleStatType)
const thresholdsListType = getThresholdsListType(colors)
const thresholdsListColors = validateThresholdsListColors(
colors,
thresholdsListType
)
const gaugeColors = validateGaugeColors(colors)
return {...state, cell, singleStatType, singleStatColors, gaugeColors}
const tableOptions = cell.tableOptions || initializeOptions('table')
return {
...state,
cell: {...cell, tableOptions},
thresholdsListType,
thresholdsListColors,
gaugeColors,
}
}
case 'HIDE_CELL_EDITOR_OVERLAY': {
@ -46,21 +59,21 @@ export default function cellEditorOverlay(state = initialState, action) {
return {...state, cell}
}
case 'UPDATE_SINGLE_STAT_COLORS': {
const {singleStatColors} = action.payload
case 'UPDATE_THRESHOLDS_LIST_COLORS': {
const {thresholdsListColors} = action.payload
return {...state, singleStatColors}
return {...state, thresholdsListColors}
}
case 'UPDATE_SINGLE_STAT_TYPE': {
const {singleStatType} = action.payload
case 'UPDATE_THRESHOLDS_LIST_TYPE': {
const {thresholdsListType} = action.payload
const singleStatColors = state.singleStatColors.map(color => ({
const thresholdsListColors = state.thresholdsListColors.map(color => ({
...color,
type: singleStatType,
type: thresholdsListType,
}))
return {...state, singleStatType, singleStatColors}
return {...state, thresholdsListType, thresholdsListColors}
}
case 'UPDATE_GAUGE_COLORS': {
@ -75,6 +88,13 @@ export default function cellEditorOverlay(state = initialState, action) {
return {...state, cell}
}
case 'UPDATE_TABLE_OPTIONS': {
const {tableOptions} = action.payload
const cell = {...state.cell, tableOptions}
return {...state, cell}
}
}
return state

View File

@ -1,17 +1,14 @@
import {writeLineProtocol as writeLineProtocolAJAX} from 'src/data_explorer/apis'
import {errorThrown} from 'shared/actions/errors'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {notify} from 'shared/actions/notifications'
import {NOTIFY_DATA_WRITTEN} from 'shared/copy/notifications'
export const writeLineProtocolAsync = (source, db, data) => async dispatch => {
try {
await writeLineProtocolAJAX(source, db, data)
dispatch(
publishAutoDismissingNotification(
'success',
'Data was written successfully'
)
)
dispatch(notify(NOTIFY_DATA_WRITTEN))
} catch (response) {
const errorMessage = `Write failed: ${response.data.error}`
dispatch(errorThrown(response, errorMessage))

View File

@ -12,6 +12,12 @@ import ManualRefresh from 'src/shared/components/ManualRefresh'
import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis'
import {getEnv} from 'src/shared/apis/env'
import {setAutoRefresh} from 'shared/actions/app'
import {notify as notifyAction} from 'shared/actions/notifications'
import {
NOTIFY_UNABLE_TO_GET_HOSTS,
NOTIFY_UNABLE_TO_GET_APPS,
} from 'shared/copy/notifications'
class HostsPage extends Component {
constructor(props) {
@ -25,9 +31,9 @@ class HostsPage extends Component {
}
async fetchHostsData() {
const {source, links, addFlashMessage} = this.props
const {source, links, notify} = this.props
const {telegrafSystemInterval} = await getEnv(links.environment)
const hostsError = 'Unable to get hosts'
const hostsError = NOTIFY_UNABLE_TO_GET_HOSTS.message
try {
const hosts = await getCpuAndLoadForHosts(
source.links.proxy,
@ -51,7 +57,7 @@ class HostsPage extends Component {
})
} catch (error) {
console.error(error)
addFlashMessage({type: 'error', text: hostsError})
notify(NOTIFY_UNABLE_TO_GET_HOSTS)
this.setState({
hostsError,
hostsLoading: false,
@ -60,14 +66,14 @@ class HostsPage extends Component {
}
async componentDidMount() {
const {addFlashMessage, autoRefresh} = this.props
const {notify, autoRefresh} = this.props
this.setState({hostsLoading: true}) // Only print this once
const {data} = await getLayouts()
this.layouts = data.layouts
if (!this.layouts) {
const layoutError = 'Unable to get apps for hosts'
addFlashMessage({type: 'error', text: layoutError})
const layoutError = NOTIFY_UNABLE_TO_GET_APPS.message
notify(NOTIFY_UNABLE_TO_GET_APPS)
this.setState({
hostsError: layoutError,
hostsLoading: false,
@ -169,11 +175,11 @@ HostsPage.propTypes = {
links: shape({
environment: string.isRequired,
}),
addFlashMessage: func,
autoRefresh: number.isRequired,
manualRefresh: number,
onChooseAutoRefresh: func.isRequired,
onManualRefresh: func.isRequired,
notify: func.isRequired,
}
HostsPage.defaultProps = {
@ -182,6 +188,7 @@ HostsPage.defaultProps = {
const mapDispatchToProps = dispatch => ({
onChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(

View File

@ -1,6 +1,6 @@
import uuid from 'uuid'
import {getActiveKapacitor} from 'shared/apis'
import {publishNotification} from 'shared/actions/notifications'
import {notify} from 'shared/actions/notifications'
import {
getRules,
getRule as getRuleAJAX,
@ -11,6 +11,17 @@ import {
} from 'src/kapacitor/apis'
import {errorThrown} from 'shared/actions/errors'
import {
NOTIFY_ALERT_RULE_DELETED,
NOTIFY_ALERT_RULE_DELETION_FAILED,
NOTIFY_ALERT_RULE_STATUS_UPDATED,
NOTIFY_ALERT_RULE_STATUS_UPDATE_FAILED,
NOTIFY_TICKSCRIPT_CREATED,
NOTIFY_TICKSCRIPT_CREATION_FAILED,
NOTIFY_TICKSCRIPT_UPDATED,
NOTIFY_TICKSCRIPT_UPDATE_FAILED,
} from 'shared/copy/notifications'
const loadQuery = query => ({
type: 'KAPA_LOAD_QUERY',
payload: {
@ -170,27 +181,21 @@ export const deleteRule = rule => dispatch => {
deleteRuleAPI(rule)
.then(() => {
dispatch(deleteRuleSuccess(rule.id))
dispatch(
publishNotification('success', `${rule.name} deleted successfully`)
)
dispatch(notify(NOTIFY_ALERT_RULE_DELETED(rule.name)))
})
.catch(() => {
dispatch(
publishNotification('error', `${rule.name} could not be deleted`)
)
dispatch(notify(NOTIFY_ALERT_RULE_DELETION_FAILED(rule.name)))
})
}
export const updateRuleStatus = (rule, status) => dispatch => {
updateRuleStatusAPI(rule, status)
.then(() => {
dispatch(
publishNotification('success', `${rule.name} ${status} successfully`)
)
dispatch(notify(NOTIFY_ALERT_RULE_STATUS_UPDATED(rule.name, status)))
})
.catch(() => {
dispatch(
publishNotification('error', `${rule.name} could not be ${status}`)
notify(NOTIFY_ALERT_RULE_STATUS_UPDATE_FAILED(rule.name, status))
)
})
}
@ -198,11 +203,11 @@ export const updateRuleStatus = (rule, status) => dispatch => {
export const createTask = (kapacitor, task) => async dispatch => {
try {
const {data} = await createTaskAJAX(kapacitor, task)
dispatch(publishNotification('success', 'TICKscript successfully created'))
dispatch(notify(NOTIFY_TICKSCRIPT_CREATED))
return data
} catch (error) {
if (!error) {
dispatch(errorThrown('Could not communicate with server'))
dispatch(errorThrown(NOTIFY_TICKSCRIPT_CREATION_FAILED))
return
}
@ -218,11 +223,11 @@ export const updateTask = (
) => async dispatch => {
try {
const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID)
dispatch(publishNotification('success', 'TICKscript saved'))
dispatch(notify(NOTIFY_TICKSCRIPT_UPDATED))
return data
} catch (error) {
if (!error) {
dispatch(errorThrown('Could not communicate with server'))
dispatch(errorThrown(NOTIFY_TICKSCRIPT_UPDATE_FAILED))
return
}
return error.data

View File

@ -4,30 +4,38 @@ import AlertTabs from 'src/kapacitor/components/AlertTabs'
import {Kapacitor, Source} from 'src/types'
type FlashMessage = {type: string; text: string}
export interface Notification {
id?: string
type: string
icon: string
duration: number
message: string
}
type NotificationFunc = () => Notification
interface AlertOutputProps {
exists: boolean
kapacitor: Kapacitor
addFlashMessage: (message: FlashMessage) => void
source: Source
hash: string
notify: (message: Notification | NotificationFunc) => void
}
const AlertOutputs: SFC<AlertOutputProps> = ({
exists,
kapacitor,
addFlashMessage,
source,
hash,
exists,
source,
kapacitor,
notify,
}) => {
if (exists) {
return (
<AlertTabs
hash={hash}
source={source}
kapacitor={kapacitor}
addFlashMessage={addFlashMessage}
hash={hash}
notify={notify}
/>
)
}

View File

@ -1,5 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
@ -23,6 +24,14 @@ import {
VictorOpsConfig,
} from './config'
import {
NOTIFY_REFRESH_KAPACITOR_FAILED,
NOTIFY_ALERT_ENDPOINT_SAVED,
NOTIFY_ALERT_ENDPOINT_SAVE_FAILED,
NOTIFY_TEST_ALERT_SENT,
NOTIFY_TEST_ALERT_FAILED,
} from 'shared/copy/notifications'
class AlertTabs extends Component {
constructor(props) {
super(props)
@ -48,10 +57,7 @@ class AlertTabs extends Component {
this.setState({configSections: sections})
} catch (error) {
this.setState({configSections: null})
this.props.addFlashMessage({
type: 'error',
text: 'There was an error getting the Kapacitor config',
})
this.props.notify(NOTIFY_REFRESH_KAPACITOR_FAILED)
}
}
@ -81,17 +87,11 @@ class AlertTabs extends Component {
propsToSend
)
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({
type: 'success',
text: `Alert configuration for ${section} successfully saved.`,
})
this.props.notify(NOTIFY_ALERT_ENDPOINT_SAVED(section))
return true
} catch ({data: {error}}) {
const errorMsg = _.join(_.drop(_.split(error, ': '), 2), ': ')
this.props.addFlashMessage({
type: 'error',
text: `There was an error saving the alert configuration for ${section}: ${errorMsg}`,
})
this.props.notify(NOTIFY_ALERT_ENDPOINT_SAVE_FAILED(section, errorMsg))
return false
}
}
@ -103,21 +103,12 @@ class AlertTabs extends Component {
try {
const {data} = await testAlertOutput(this.props.kapacitor, section)
if (data.success) {
this.props.addFlashMessage({
type: 'success',
text: `Successfully triggered an alert to ${section}. If the alert does not reach its destination, please check your configuration settings.`,
})
this.props.notify(NOTIFY_TEST_ALERT_SENT(section))
} else {
this.props.addFlashMessage({
type: 'error',
text: `There was an error sending an alert to ${section}: ${data.message}`,
})
this.props.notify(NOTIFY_TEST_ALERT_FAILED(section, data.message))
}
} catch (error) {
this.props.addFlashMessage({
type: 'error',
text: `There was an error sending an alert to ${section}.`,
})
this.props.notify(NOTIFY_TEST_ALERT_FAILED(section))
}
}
@ -329,7 +320,7 @@ AlertTabs.propTypes = {
proxy: string.isRequired,
}).isRequired,
}),
addFlashMessage: func.isRequired,
notify: func.isRequired,
hash: string.isRequired,
}

View File

@ -97,7 +97,6 @@ DataSection.propTypes = {
query: shape({
id: string.isRequired,
}).isRequired,
addFlashMessage: func,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,

View File

@ -4,9 +4,18 @@ import AlertOutputs from 'src/kapacitor/components/AlertOutputs'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import Input from 'src/kapacitor/components/KapacitorFormInput'
import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText'
import {Kapacitor, Source} from 'src/types'
type FlashMessage = {type: string; text: string}
export interface Notification {
id?: string
type: string
icon: string
duration: number
message: string
}
type NotificationFunc = () => Notification
interface Props {
kapacitor: Kapacitor
@ -14,23 +23,25 @@ interface Props {
onReset: (e: MouseEvent<HTMLButtonElement>) => void
onSubmit: (e: ChangeEvent<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
onCheckboxChange: (e: ChangeEvent<HTMLInputElement>) => void
onChangeUrl: (e: ChangeEvent<HTMLInputElement>) => void
addFlashMessage: (message: FlashMessage) => void
source: Source
hash: string
notify: (message: Notification | NotificationFunc) => void
}
const KapacitorForm: SFC<Props> = ({
onChangeUrl,
onReset,
kapacitor,
kapacitor: {url, name, username, password},
kapacitor: {url, name, username, password, insecureSkipVerify},
onSubmit,
exists,
onInputChange,
addFlashMessage,
onCheckboxChange,
source,
hash,
notify,
}) =>
<div className="page">
<div className="page-header">
@ -84,6 +95,22 @@ const KapacitorForm: SFC<Props> = ({
inputType="password"
/>
</div>
{url.startsWith('https') &&
<div className="form-group col-xs-12">
<div className="form-control-static">
<input
type="checkbox"
id="insecureSkipVerifyCheckbox"
name="insecureSkipVerify"
checked={insecureSkipVerify}
onChange={onCheckboxChange}
/>
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
</div>
<label className="form-helper">
{insecureSkipVerifyText}
</label>
</div>}
<div className="form-group form-group-submit col-xs-12 text-center">
<button
className="btn btn-default"
@ -106,15 +133,13 @@ const KapacitorForm: SFC<Props> = ({
</div>
</div>
<div className="col-md-9">
{
<AlertOutputs
exists={exists}
kapacitor={kapacitor}
addFlashMessage={addFlashMessage}
source={source}
hash={hash}
/>
}
<AlertOutputs
hash={hash}
exists={exists}
source={source}
kapacitor={kapacitor}
notify={notify}
/>
</div>
</div>
</div>

View File

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

View File

@ -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<Props, State> {
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<Props, State> {
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<HTMLInputElement>) => {
const {checked} = e.target
this.setState({
kapacitor: {
...this.state.kapacitor,
insecureSkipVerify: checked
}
})
}
handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const {value, name} = e.target
@ -97,7 +118,7 @@ export class KapacitorPage extends PureComponent<Props, State> {
handleSubmit = async e => {
e.preventDefault()
const {
addFlashMessage,
notify,
source,
source: {kapacitors = []},
params,
@ -110,10 +131,7 @@ export class KapacitorPage extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
}
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<Props, State> {
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))

View File

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

View File

@ -78,7 +78,6 @@ KapacitorRulesPage.propTypes = {
deleteRule: func.isRequired,
updateRuleStatus: func.isRequired,
}).isRequired,
addFlashMessage: func,
}
const mapStateToProps = state => {

View File

@ -12,7 +12,6 @@ export const KapacitorTasksPage = React.createClass({
kapacitors: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
addFlashMessage: PropTypes.func,
},
getInitialState() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
? <div className={wrapperClass}>
<div className="input-cte__disabled">
{value}
</div>
</div>
: <div className={wrapperClass}>
{isEditing
? <input
type="text"
className="form-control input-sm provider--input"
defaultValue={value}
onBlur={this.handleInputBlur}
onKeyDown={this.handleKeyDown}
autoFocus={true}
onFocus={this.handleFocus}
ref={r => (this.inputRef = r)}
tabIndex={tabIndex}
spellCheck={false}
/>
: <div
className={defaultStyle}
onClick={this.handleInputClick}
onFocus={this.handleInputClick}
tabIndex={tabIndex}
>
{value || placeholder}
{appearAsNormalInput || <span className="icon pencil" />}
</div>}
</div>
}
}
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

View File

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

View File

@ -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 (
<div
className={notificationContainerClass}
style={{height: height + notificationMargin}}
>
<div
className={notificationClass}
ref={r => (this.notificationRef = r)}
>
<span className={`icon ${icon}`} />
<div className="notification-message">
{message}
</div>
<button className="notification-close" onClick={this.handleDismiss} />
</div>
</div>
)
}
}
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)

View File

@ -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}) =>
<div
className={`${inPresentationMode
? 'notification-center__presentation-mode'
: 'notification-center'}`}
>
{notifications.map(n => <Notification key={n.id} notification={n} />)}
</div>
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 (
<div className={cls} role="alert">
{message}
{this.renderDismiss(type)}
</div>
)
}
handleDismiss = type => () => this.props.dismissNotification(type)
renderDismiss = type => {
return (
<button
className="close"
data-dismiss="alert"
aria-label="Close"
onClick={this.handleDismiss(type)}
>
<span className="icon remove" />
</button>
)
}
render() {
const {success, error, warning} = this.props.notifications
if (!success && !error && !warning) {
return null
}
return (
<div className="flash-messages">
{this.renderNotification('success', success)}
{this.renderNotification('error', error)}
{this.renderNotification('warning', warning)}
</div>
)
}
}
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)

View File

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

View File

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

View File

@ -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 (
<div
key={key}
style={style}
style={cellStyle}
className={cellClass}
onMouseOver={this.handleHover(columnIndex, rowIndex)}
>
{`${data[rowIndex][columnIndex]}`}
{isTimeData
? `${moment(cellData).format(timeFormat)}`
: columnName || `${cellData}`}
</div>
)
}
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}
/>}
</div>
)
@ -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

View File

@ -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 (
<div className={thresholdsListClass}>
{showListHeading && <label className="form-label">Thresholds</label>}
<button
className="btn btn-sm btn-primary"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>
<span className="icon plus" /> Add Threshold
</button>
{thresholdsListColors.map(
color =>
color.id === THRESHOLD_TYPE_BASE
? <div className="threshold-item" key={uuid.v4()}>
<div className="threshold-item--label">Base Color</div>
<ColorDropdown
colors={THRESHOLD_COLORS}
selected={formatColor(color)}
onChoose={this.handleChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onDeleteThreshold={this.handleDeleteThreshold}
onSortColors={this.handleSortColors}
/>
)}
</div>
)
}
}
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)

View File

@ -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 (
<div className={containerClass}>
<label>Threshold Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${thresholdsListType === THRESHOLD_TYPE_BG
? 'active'
: ''}`}
onClick={this.handleToggleThresholdsListType(THRESHOLD_TYPE_BG)}
>
Background
</li>
<li
className={`${thresholdsListType === THRESHOLD_TYPE_TEXT
? 'active'
: ''}`}
onClick={this.handleToggleThresholdsListType(THRESHOLD_TYPE_TEXT)}
>
Text
</li>
</ul>
</div>
)
}
}
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
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More