diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f0d53654..d71a383a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ 1. [#2327](https://github.com/influxdata/chronograf/pull/2327): After CREATE/DELETE queries, refresh list of databases in Data Explorer 1. [#2327](https://github.com/influxdata/chronograf/pull/2327): Visualize CREATE/DELETE queries with Table view in Data Explorer 1. [#2329](https://github.com/influxdata/chronograf/pull/2329): Include tag values alongside measurement name in Data Explorer result tabs +1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Redesign cell display options panel +1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Introduce customizable Gauge visualization type for dashboard cells 1. [#2386](https://github.com/influxdata/chronograf/pull/2386): Fix queries that include regex, numbers and wildcard 1. [#2398](https://github.com/influxdata/chronograf/pull/2398): Fix apps on hosts page from parsing tags with null values 1. [#2408](https://github.com/influxdata/chronograf/pull/2408): Fix updated Dashboard names not updating dashboard list @@ -37,6 +39,7 @@ 1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering ### UI Improvements +1. [#2427](https://github.com/influxdata/chronograf/pull/2427): Improve performance of Hosts, Alert History, and TICKscript logging pages when there are many items to display ## v1.3.10.0 [2017-10-24] ### Bug Fixes diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index 9cb243be0e..c657ef6783 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -219,6 +219,17 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { queries[j].Shifts = shifts } + colors := make([]*Color, len(c.CellColors)) + for j, color := range c.CellColors { + colors[j] = &Color{ + ID: color.ID, + Type: color.Type, + Hex: color.Hex, + Name: color.Name, + Value: color.Value, + } + } + axes := make(map[string]*Axis, len(c.Axes)) for a, r := range c.Axes { axes[a] = &Axis{ @@ -241,6 +252,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { Queries: queries, Type: c.Type, Axes: axes, + Colors: colors, } } templates := make([]*Template, len(d.Templates)) @@ -320,6 +332,17 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { queries[j].Shifts = shifts } + colors := make([]chronograf.CellColor, len(c.Colors)) + for j, color := range c.Colors { + colors[j] = chronograf.CellColor{ + ID: color.ID, + Type: color.Type, + Hex: color.Hex, + Name: color.Name, + Value: color.Value, + } + } + axes := make(map[string]chronograf.Axis, len(c.Axes)) for a, r := range c.Axes { // axis base defaults to 10 @@ -351,15 +374,16 @@ 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: c.Type, - Axes: axes, + ID: c.ID, + X: c.X, + Y: c.Y, + W: c.W, + H: c.H, + Name: c.Name, + Queries: queries, + Type: c.Type, + Axes: axes, + CellColors: colors, } } diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index 01fbd30e30..91eb31bf7f 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -12,6 +12,7 @@ It has these top-level messages: Source Dashboard DashboardCell + Color Axis Template TemplateValue @@ -102,6 +103,7 @@ type DashboardCell struct { 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"` } func (m *DashboardCell) Reset() { *m = DashboardCell{} } @@ -123,6 +125,26 @@ func (m *DashboardCell) GetAxes() map[string]*Axis { return nil } +func (m *DashboardCell) GetColors() []*Color { + if m != nil { + return m.Colors + } + return nil +} + +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"` + Hex string `protobuf:"bytes,3,opt,name=Hex,proto3" json:"Hex,omitempty"` + Name string `protobuf:"bytes,4,opt,name=Name,proto3" json:"Name,omitempty"` + Value string `protobuf:"bytes,5,opt,name=Value,proto3" json:"Value,omitempty"` +} + +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} } + type Axis struct { LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"` Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"` @@ -136,7 +158,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{3} } +func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } type Template struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -150,7 +172,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{4} } +func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } func (m *Template) GetValues() []*TemplateValue { if m != nil { @@ -175,7 +197,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{5} } +func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } type TemplateQuery struct { Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` @@ -189,7 +211,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{6} } +func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } type Server struct { ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -205,7 +227,7 @@ type Server struct { func (m *Server) Reset() { *m = Server{} } func (m *Server) String() string { return proto.CompactTextString(m) } func (*Server) ProtoMessage() {} -func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } +func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } type Layout struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -218,7 +240,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{8} } +func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} } func (m *Layout) GetCells() []*Cell { if m != nil { @@ -244,7 +266,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{9} } +func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} } func (m *Cell) GetQueries() []*Query { if m != nil { @@ -275,7 +297,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{10} } +func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} } func (m *Query) GetRange() *Range { if m != nil { @@ -300,7 +322,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{11} } +func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} } type Range struct { Upper int64 `protobuf:"varint,1,opt,name=Upper,proto3" json:"Upper,omitempty"` @@ -310,7 +332,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{12} } +func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} } type AlertRule struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -322,7 +344,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{13} } +func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} } type User struct { ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -336,7 +358,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{14} } +func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} } func (m *User) GetRoles() []*Role { if m != nil { @@ -353,7 +375,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{15} } +func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} } type Organization struct { ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -365,12 +387,13 @@ 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{16} } +func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} } func init() { proto.RegisterType((*Source)(nil), "internal.Source") proto.RegisterType((*Dashboard)(nil), "internal.Dashboard") proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell") + proto.RegisterType((*Color)(nil), "internal.Color") proto.RegisterType((*Axis)(nil), "internal.Axis") proto.RegisterType((*Template)(nil), "internal.Template") proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue") @@ -390,81 +413,84 @@ func init() { func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } var fileDescriptorInternal = []byte{ - // 1207 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0x5f, 0x8f, 0xdb, 0x44, - 0x10, 0xd7, 0xc6, 0x71, 0x62, 0x4f, 0xae, 0x05, 0x2d, 0x15, 0x35, 0x45, 0x42, 0xc1, 0x02, 0xe9, - 0x10, 0xf4, 0x40, 0xad, 0x90, 0x10, 0x0f, 0x48, 0xb9, 0x0b, 0xaa, 0x8e, 0xfe, 0xbb, 0x6e, 0x7a, - 0xe5, 0x09, 0x55, 0x1b, 0x67, 0x72, 0xb1, 0xea, 0xd8, 0x66, 0x6d, 0xdf, 0x9d, 0xf9, 0x30, 0x48, - 0x48, 0x3c, 0xf1, 0x88, 0x78, 0xe7, 0x15, 0xf1, 0x41, 0xf8, 0x0a, 0xbc, 0xa2, 0xd9, 0x5d, 0x3b, - 0x4e, 0x2f, 0x54, 0x7d, 0x81, 0xb7, 0xfd, 0xcd, 0xac, 0x67, 0x77, 0x66, 0x7e, 0xf3, 0xf3, 0xc2, - 0xf5, 0x38, 0x2d, 0x51, 0xa5, 0x32, 0x39, 0xc8, 0x55, 0x56, 0x66, 0xdc, 0x6b, 0x70, 0xf8, 0x57, - 0x0f, 0x06, 0xb3, 0xac, 0x52, 0x11, 0xf2, 0xeb, 0xd0, 0x3b, 0x9e, 0x06, 0x6c, 0xcc, 0xf6, 0x1d, - 0xd1, 0x3b, 0x9e, 0x72, 0x0e, 0xfd, 0x47, 0x72, 0x8d, 0x41, 0x6f, 0xcc, 0xf6, 0x7d, 0xa1, 0xd7, - 0x64, 0x7b, 0x5a, 0xe7, 0x18, 0x38, 0xc6, 0x46, 0x6b, 0x7e, 0x0b, 0xbc, 0xd3, 0x82, 0xa2, 0xad, - 0x31, 0xe8, 0x6b, 0x7b, 0x8b, 0xc9, 0x77, 0x22, 0x8b, 0xe2, 0x22, 0x53, 0x8b, 0xc0, 0x35, 0xbe, - 0x06, 0xf3, 0x37, 0xc1, 0x39, 0x15, 0x0f, 0x82, 0x81, 0x36, 0xd3, 0x92, 0x07, 0x30, 0x9c, 0xe2, - 0x52, 0x56, 0x49, 0x19, 0x0c, 0xc7, 0x6c, 0xdf, 0x13, 0x0d, 0xa4, 0x38, 0x4f, 0x31, 0xc1, 0x33, - 0x25, 0x97, 0x81, 0x67, 0xe2, 0x34, 0x98, 0x1f, 0x00, 0x3f, 0x4e, 0x0b, 0x8c, 0x2a, 0x85, 0xb3, - 0x17, 0x71, 0xfe, 0x0c, 0x55, 0xbc, 0xac, 0x03, 0x5f, 0x07, 0xd8, 0xe1, 0xa1, 0x53, 0x1e, 0x62, - 0x29, 0xe9, 0x6c, 0xd0, 0xa1, 0x1a, 0xc8, 0x43, 0xd8, 0x9b, 0xad, 0xa4, 0xc2, 0xc5, 0x0c, 0x23, - 0x85, 0x65, 0x30, 0xd2, 0xee, 0x2d, 0x1b, 0xed, 0x79, 0xac, 0xce, 0x64, 0x1a, 0xff, 0x20, 0xcb, - 0x38, 0x4b, 0x83, 0x3d, 0xb3, 0xa7, 0x6b, 0xa3, 0x2a, 0x89, 0x2c, 0xc1, 0xe0, 0x9a, 0xa9, 0x12, - 0xad, 0xc3, 0xdf, 0x18, 0xf8, 0x53, 0x59, 0xac, 0xe6, 0x99, 0x54, 0x8b, 0xd7, 0xaa, 0xf5, 0x6d, - 0x70, 0x23, 0x4c, 0x92, 0x22, 0x70, 0xc6, 0xce, 0xfe, 0xe8, 0xce, 0xcd, 0x83, 0xb6, 0x89, 0x6d, - 0x9c, 0x23, 0x4c, 0x12, 0x61, 0x76, 0xf1, 0xcf, 0xc0, 0x2f, 0x71, 0x9d, 0x27, 0xb2, 0xc4, 0x22, - 0xe8, 0xeb, 0x4f, 0xf8, 0xe6, 0x93, 0xa7, 0xd6, 0x25, 0x36, 0x9b, 0xae, 0xa4, 0xe2, 0x5e, 0x4d, - 0x25, 0xfc, 0xa5, 0x07, 0xd7, 0xb6, 0x8e, 0xe3, 0x7b, 0xc0, 0x2e, 0xf5, 0xcd, 0x5d, 0xc1, 0x2e, - 0x09, 0xd5, 0xfa, 0xd6, 0xae, 0x60, 0x35, 0xa1, 0x0b, 0xcd, 0x0d, 0x57, 0xb0, 0x0b, 0x42, 0x2b, - 0xcd, 0x08, 0x57, 0xb0, 0x15, 0xff, 0x08, 0x86, 0xdf, 0x57, 0xa8, 0x62, 0x2c, 0x02, 0x57, 0xdf, - 0xee, 0x8d, 0xcd, 0xed, 0x9e, 0x54, 0xa8, 0x6a, 0xd1, 0xf8, 0xa9, 0x1a, 0x9a, 0x4d, 0x86, 0x1a, - 0x7a, 0x4d, 0xb6, 0x92, 0x98, 0x37, 0x34, 0x36, 0x5a, 0xdb, 0x2a, 0x1a, 0x3e, 0x50, 0x15, 0x3f, - 0x87, 0xbe, 0xbc, 0xc4, 0x22, 0xf0, 0x75, 0xfc, 0xf7, 0xff, 0xa5, 0x60, 0x07, 0x93, 0x4b, 0x2c, - 0xbe, 0x4e, 0x4b, 0x55, 0x0b, 0xbd, 0xfd, 0xd6, 0x3d, 0xf0, 0x5b, 0x13, 0xb1, 0xf2, 0x05, 0xd6, - 0x3a, 0x41, 0x5f, 0xd0, 0x92, 0x7f, 0x00, 0xee, 0xb9, 0x4c, 0x2a, 0xd3, 0x9c, 0xd1, 0x9d, 0xeb, - 0x9b, 0xb0, 0x93, 0xcb, 0xb8, 0x10, 0xc6, 0xf9, 0x65, 0xef, 0x0b, 0x16, 0xfe, 0xca, 0xa0, 0x4f, - 0x36, 0xaa, 0x6c, 0x82, 0x67, 0x32, 0xaa, 0x0f, 0xb3, 0x2a, 0x5d, 0x14, 0x01, 0x1b, 0x3b, 0xfb, - 0x8e, 0xd8, 0xb2, 0xf1, 0xb7, 0x61, 0x30, 0x37, 0xde, 0xde, 0xd8, 0xd9, 0xf7, 0x85, 0x45, 0xfc, - 0x06, 0xb8, 0x89, 0x9c, 0x63, 0x62, 0x67, 0xcc, 0x00, 0xda, 0x9d, 0x2b, 0x5c, 0xc6, 0x97, 0x76, - 0xc4, 0x2c, 0x22, 0x7b, 0x51, 0x2d, 0xc9, 0x6e, 0xba, 0x67, 0x11, 0x95, 0x6b, 0x2e, 0x8b, 0xb6, - 0x84, 0xb4, 0xa6, 0xc8, 0x45, 0x24, 0x93, 0xa6, 0x86, 0x06, 0x84, 0xbf, 0x33, 0x9a, 0x2d, 0xc3, - 0x89, 0x0e, 0x2f, 0x4d, 0x45, 0xdf, 0x01, 0x8f, 0xf8, 0xf2, 0xfc, 0x5c, 0x2a, 0xcb, 0xcd, 0x21, - 0xe1, 0x67, 0x52, 0xf1, 0x4f, 0x61, 0xa0, 0x33, 0xdf, 0xc1, 0xcf, 0x26, 0xdc, 0x33, 0xf2, 0x0b, - 0xbb, 0xad, 0xed, 0x60, 0xbf, 0xd3, 0xc1, 0x36, 0x59, 0xb7, 0x9b, 0xec, 0x6d, 0x70, 0x89, 0x0a, - 0xb5, 0xbe, 0xfd, 0xce, 0xc8, 0x86, 0x30, 0x66, 0x57, 0x78, 0x0a, 0xd7, 0xb6, 0x4e, 0x6c, 0x4f, - 0x62, 0xdb, 0x27, 0x6d, 0xba, 0xe8, 0xdb, 0xae, 0x91, 0xae, 0x14, 0x98, 0x60, 0x54, 0xe2, 0x42, - 0xd7, 0xdb, 0x13, 0x2d, 0x0e, 0x7f, 0x62, 0x9b, 0xb8, 0xfa, 0x3c, 0x52, 0x8e, 0x28, 0x5b, 0xaf, - 0x65, 0xba, 0xb0, 0xa1, 0x1b, 0x48, 0x75, 0x5b, 0xcc, 0x6d, 0xe8, 0xde, 0x62, 0x4e, 0x58, 0xe5, - 0xb6, 0x83, 0x3d, 0x95, 0xf3, 0x31, 0x8c, 0xd6, 0x28, 0x8b, 0x4a, 0xe1, 0x1a, 0xd3, 0xd2, 0x96, - 0xa0, 0x6b, 0xe2, 0x37, 0x61, 0x58, 0xca, 0xb3, 0xe7, 0xc4, 0x3d, 0xdb, 0xc9, 0x52, 0x9e, 0xdd, - 0xc7, 0x9a, 0xbf, 0x0b, 0xfe, 0x32, 0xc6, 0x64, 0xa1, 0x5d, 0xa6, 0x9d, 0x9e, 0x36, 0xdc, 0xc7, - 0x3a, 0xfc, 0x83, 0xc1, 0x60, 0x86, 0xea, 0x1c, 0xd5, 0x6b, 0x49, 0x4a, 0x57, 0xaa, 0x9d, 0x57, - 0x48, 0x75, 0x7f, 0xb7, 0x54, 0xbb, 0x1b, 0xa9, 0xbe, 0x01, 0xee, 0x4c, 0x45, 0xc7, 0x53, 0x7d, - 0x23, 0x47, 0x18, 0x40, 0x6c, 0x9c, 0x44, 0x65, 0x7c, 0x8e, 0x56, 0xbf, 0x2d, 0xba, 0xa2, 0x34, - 0xde, 0x0e, 0xa5, 0xf9, 0x91, 0xc1, 0xe0, 0x81, 0xac, 0xb3, 0xaa, 0xbc, 0xc2, 0xc2, 0x31, 0x8c, - 0x26, 0x79, 0x9e, 0xc4, 0x91, 0xf9, 0xda, 0x64, 0xd4, 0x35, 0xd1, 0x8e, 0x87, 0x9d, 0xfa, 0x9a, - 0xdc, 0xba, 0x26, 0x9a, 0xe2, 0x23, 0xad, 0xa6, 0x46, 0x1a, 0x3b, 0x53, 0x6c, 0x44, 0x54, 0x3b, - 0xa9, 0x08, 0x93, 0xaa, 0xcc, 0x96, 0x49, 0x76, 0xa1, 0xb3, 0xf5, 0x44, 0x8b, 0xc3, 0x3f, 0x7b, - 0xd0, 0xff, 0xbf, 0x14, 0x70, 0x0f, 0x58, 0x6c, 0x9b, 0xcd, 0xe2, 0x56, 0x0f, 0x87, 0x1d, 0x3d, - 0x0c, 0x60, 0x58, 0x2b, 0x99, 0x9e, 0x61, 0x11, 0x78, 0x5a, 0x5d, 0x1a, 0xa8, 0x3d, 0x7a, 0x8e, - 0x8c, 0x10, 0xfa, 0xa2, 0x81, 0xed, 0x5c, 0x40, 0x67, 0x2e, 0x3e, 0xb1, 0x9a, 0x39, 0xd2, 0x37, - 0x0a, 0xb6, 0xcb, 0xf2, 0xdf, 0x49, 0xe5, 0xdf, 0x0c, 0xdc, 0x76, 0xa8, 0x8e, 0xb6, 0x87, 0xea, - 0x68, 0x33, 0x54, 0xd3, 0xc3, 0x66, 0xa8, 0xa6, 0x87, 0x84, 0xc5, 0x49, 0x33, 0x54, 0xe2, 0x84, - 0x9a, 0x75, 0x4f, 0x65, 0x55, 0x7e, 0x58, 0x9b, 0xae, 0xfa, 0xa2, 0xc5, 0xc4, 0xc4, 0x6f, 0x57, - 0xa8, 0x6c, 0xa9, 0x7d, 0x61, 0x11, 0xf1, 0xf6, 0x81, 0x16, 0x1c, 0x53, 0x5c, 0x03, 0xf8, 0x87, - 0xe0, 0x0a, 0x2a, 0x9e, 0xae, 0xf0, 0x56, 0x5f, 0xb4, 0x59, 0x18, 0x2f, 0x05, 0x35, 0x6f, 0x25, - 0x4b, 0xe0, 0xe6, 0xe5, 0xf4, 0x31, 0x0c, 0x66, 0xab, 0x78, 0x59, 0x36, 0x7f, 0x9e, 0xb7, 0x3a, - 0x82, 0x15, 0xaf, 0x51, 0xfb, 0x84, 0xdd, 0x12, 0x3e, 0x01, 0xbf, 0x35, 0x6e, 0xae, 0xc3, 0xba, - 0xd7, 0xe1, 0xd0, 0x3f, 0x4d, 0xe3, 0xb2, 0x19, 0x5d, 0x5a, 0x53, 0xb2, 0x4f, 0x2a, 0x99, 0x96, - 0x71, 0x59, 0x37, 0xa3, 0xdb, 0xe0, 0xf0, 0xae, 0xbd, 0x3e, 0x85, 0x3b, 0xcd, 0x73, 0x54, 0x56, - 0x06, 0x0c, 0xd0, 0x87, 0x64, 0x17, 0x68, 0x14, 0xdc, 0x11, 0x06, 0x84, 0xdf, 0x81, 0x3f, 0x49, - 0x50, 0x95, 0xa2, 0x4a, 0xae, 0xea, 0x3e, 0x87, 0xfe, 0x37, 0xb3, 0xc7, 0x8f, 0x9a, 0x1b, 0xd0, - 0x7a, 0x33, 0xf2, 0xce, 0x4b, 0x23, 0x7f, 0x5f, 0xe6, 0xf2, 0x78, 0xaa, 0x79, 0xee, 0x08, 0x8b, - 0xc2, 0x9f, 0x19, 0xf4, 0x49, 0x5b, 0x3a, 0xa1, 0xfb, 0xaf, 0xd2, 0xa5, 0x13, 0x95, 0x9d, 0xc7, - 0x0b, 0x54, 0x4d, 0x72, 0x0d, 0xd6, 0x45, 0x8f, 0x56, 0xd8, 0x3e, 0x2e, 0x2d, 0x22, 0xae, 0xd1, - 0xc3, 0xaa, 0x99, 0xa5, 0x0e, 0xd7, 0xc8, 0x2c, 0x8c, 0x93, 0xbf, 0x07, 0x30, 0xab, 0x72, 0x54, - 0x93, 0xc5, 0x3a, 0x4e, 0x75, 0xd3, 0x3d, 0xd1, 0xb1, 0x84, 0x5f, 0x99, 0xa7, 0xda, 0x15, 0x85, - 0x62, 0xbb, 0x9f, 0x75, 0x2f, 0xdf, 0x3c, 0x4c, 0xb6, 0xbf, 0x7b, 0xad, 0x6c, 0xc7, 0x30, 0xb2, - 0xef, 0x5a, 0xfd, 0x4a, 0xb4, 0x62, 0xd5, 0x31, 0x51, 0xce, 0x27, 0xd5, 0x3c, 0x89, 0x23, 0x9d, - 0xb3, 0x27, 0x2c, 0x9a, 0x0f, 0xf4, 0xf3, 0xfd, 0xee, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xaa, - 0x43, 0x90, 0xf1, 0xd0, 0x0b, 0x00, 0x00, + // 1264 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x57, 0xdf, 0x8e, 0xdb, 0xc4, + 0x17, 0x96, 0xe3, 0x38, 0xb1, 0x4f, 0xb6, 0xfd, 0x55, 0xf3, 0xab, 0xa8, 0x29, 0x12, 0x0a, 0x16, + 0x88, 0x45, 0xd0, 0x05, 0xb5, 0x42, 0x42, 0x5c, 0x20, 0x65, 0x37, 0xa8, 0x2c, 0xfd, 0xb7, 0x9d, + 0x74, 0xcb, 0x15, 0xaa, 0x26, 0xce, 0x49, 0x62, 0xd5, 0xb1, 0xcd, 0xd8, 0xde, 0x8d, 0x79, 0x18, + 0x24, 0x24, 0x9e, 0x00, 0x71, 0xcf, 0x2d, 0xe2, 0x96, 0x77, 0xe0, 0x15, 0xb8, 0x45, 0x67, 0x66, + 0xec, 0x38, 0x9b, 0x50, 0xf5, 0x02, 0x71, 0x37, 0xdf, 0x39, 0x93, 0x33, 0x67, 0xce, 0xf9, 0xce, + 0x37, 0x0e, 0x5c, 0x8f, 0x92, 0x02, 0x65, 0x22, 0xe2, 0xa3, 0x4c, 0xa6, 0x45, 0xca, 0xdc, 0x1a, + 0x07, 0x7f, 0x76, 0xa0, 0x37, 0x49, 0x4b, 0x19, 0x22, 0xbb, 0x0e, 0x9d, 0xd3, 0xb1, 0x6f, 0x0d, + 0xad, 0x43, 0x9b, 0x77, 0x4e, 0xc7, 0x8c, 0x41, 0xf7, 0xb1, 0x58, 0xa1, 0xdf, 0x19, 0x5a, 0x87, + 0x1e, 0x57, 0x6b, 0xb2, 0x3d, 0xab, 0x32, 0xf4, 0x6d, 0x6d, 0xa3, 0x35, 0xbb, 0x0d, 0xee, 0x79, + 0x4e, 0xd1, 0x56, 0xe8, 0x77, 0x95, 0xbd, 0xc1, 0xe4, 0x3b, 0x13, 0x79, 0x7e, 0x99, 0xca, 0x99, + 0xef, 0x68, 0x5f, 0x8d, 0xd9, 0x0d, 0xb0, 0xcf, 0xf9, 0x43, 0xbf, 0xa7, 0xcc, 0xb4, 0x64, 0x3e, + 0xf4, 0xc7, 0x38, 0x17, 0x65, 0x5c, 0xf8, 0xfd, 0xa1, 0x75, 0xe8, 0xf2, 0x1a, 0x52, 0x9c, 0x67, + 0x18, 0xe3, 0x42, 0x8a, 0xb9, 0xef, 0xea, 0x38, 0x35, 0x66, 0x47, 0xc0, 0x4e, 0x93, 0x1c, 0xc3, + 0x52, 0xe2, 0xe4, 0x65, 0x94, 0x3d, 0x47, 0x19, 0xcd, 0x2b, 0xdf, 0x53, 0x01, 0xf6, 0x78, 0xe8, + 0x94, 0x47, 0x58, 0x08, 0x3a, 0x1b, 0x54, 0xa8, 0x1a, 0xb2, 0x00, 0x0e, 0x26, 0x4b, 0x21, 0x71, + 0x36, 0xc1, 0x50, 0x62, 0xe1, 0x0f, 0x94, 0x7b, 0xcb, 0x46, 0x7b, 0x9e, 0xc8, 0x85, 0x48, 0xa2, + 0xef, 0x45, 0x11, 0xa5, 0x89, 0x7f, 0xa0, 0xf7, 0xb4, 0x6d, 0x54, 0x25, 0x9e, 0xc6, 0xe8, 0x5f, + 0xd3, 0x55, 0xa2, 0x75, 0xf0, 0x8b, 0x05, 0xde, 0x58, 0xe4, 0xcb, 0x69, 0x2a, 0xe4, 0xec, 0xb5, + 0x6a, 0x7d, 0x07, 0x9c, 0x10, 0xe3, 0x38, 0xf7, 0xed, 0xa1, 0x7d, 0x38, 0xb8, 0x7b, 0xeb, 0xa8, + 0x69, 0x62, 0x13, 0xe7, 0x04, 0xe3, 0x98, 0xeb, 0x5d, 0xec, 0x13, 0xf0, 0x0a, 0x5c, 0x65, 0xb1, + 0x28, 0x30, 0xf7, 0xbb, 0xea, 0x27, 0x6c, 0xf3, 0x93, 0x67, 0xc6, 0xc5, 0x37, 0x9b, 0x76, 0xae, + 0xe2, 0xec, 0x5e, 0x25, 0xf8, 0xa3, 0x03, 0xd7, 0xb6, 0x8e, 0x63, 0x07, 0x60, 0xad, 0x55, 0xe6, + 0x0e, 0xb7, 0xd6, 0x84, 0x2a, 0x95, 0xb5, 0xc3, 0xad, 0x8a, 0xd0, 0xa5, 0xe2, 0x86, 0xc3, 0xad, + 0x4b, 0x42, 0x4b, 0xc5, 0x08, 0x87, 0x5b, 0x4b, 0xf6, 0x01, 0xf4, 0xbf, 0x2b, 0x51, 0x46, 0x98, + 0xfb, 0x8e, 0xca, 0xee, 0x7f, 0x9b, 0xec, 0x9e, 0x96, 0x28, 0x2b, 0x5e, 0xfb, 0xa9, 0x1a, 0x8a, + 0x4d, 0x9a, 0x1a, 0x6a, 0x4d, 0xb6, 0x82, 0x98, 0xd7, 0xd7, 0x36, 0x5a, 0x9b, 0x2a, 0x6a, 0x3e, + 0x50, 0x15, 0x3f, 0x85, 0xae, 0x58, 0x63, 0xee, 0x7b, 0x2a, 0xfe, 0x3b, 0xff, 0x50, 0xb0, 0xa3, + 0xd1, 0x1a, 0xf3, 0x2f, 0x93, 0x42, 0x56, 0x5c, 0x6d, 0x67, 0xef, 0x43, 0x2f, 0x4c, 0xe3, 0x54, + 0xe6, 0x3e, 0x5c, 0x4d, 0xec, 0x84, 0xec, 0xdc, 0xb8, 0x6f, 0xdf, 0x07, 0xaf, 0xf9, 0x2d, 0xd1, + 0xf7, 0x25, 0x56, 0xaa, 0x12, 0x1e, 0xa7, 0x25, 0x7b, 0x17, 0x9c, 0x0b, 0x11, 0x97, 0xba, 0x8b, + 0x83, 0xbb, 0xd7, 0x37, 0x61, 0x46, 0xeb, 0x28, 0xe7, 0xda, 0xf9, 0x79, 0xe7, 0x33, 0x2b, 0x58, + 0x80, 0xa3, 0x22, 0xb7, 0x78, 0xe0, 0xd5, 0x3c, 0x50, 0xf3, 0xd5, 0x69, 0xcd, 0xd7, 0x0d, 0xb0, + 0xbf, 0xc2, 0xb5, 0x19, 0x39, 0x5a, 0x36, 0x6c, 0xe9, 0xb6, 0xd8, 0x72, 0x13, 0x9c, 0xe7, 0xea, + 0x70, 0xdd, 0x45, 0x0d, 0x82, 0x9f, 0x2d, 0xe8, 0xd2, 0xe1, 0xd4, 0xeb, 0x18, 0x17, 0x22, 0xac, + 0x8e, 0xd3, 0x32, 0x99, 0xe5, 0xbe, 0x35, 0xb4, 0x0f, 0x6d, 0xbe, 0x65, 0x63, 0x6f, 0x40, 0x6f, + 0xaa, 0xbd, 0x9d, 0xa1, 0x7d, 0xe8, 0x71, 0x83, 0x28, 0x74, 0x2c, 0xa6, 0x18, 0x9b, 0x14, 0x34, + 0xa0, 0xdd, 0x99, 0xc4, 0x79, 0xb4, 0x36, 0x69, 0x18, 0x44, 0xf6, 0xbc, 0x9c, 0x93, 0x5d, 0x67, + 0x62, 0x10, 0x25, 0x3d, 0x15, 0x79, 0xd3, 0x54, 0x5a, 0x53, 0xe4, 0x3c, 0x14, 0x71, 0xdd, 0x55, + 0x0d, 0x82, 0x5f, 0x2d, 0x9a, 0x76, 0xcd, 0xd2, 0x9d, 0x0a, 0xbd, 0x09, 0x2e, 0x31, 0xf8, 0xc5, + 0x85, 0x90, 0xa6, 0x4a, 0x7d, 0xc2, 0xcf, 0x85, 0x64, 0x1f, 0x43, 0x4f, 0x95, 0x78, 0xcf, 0xc4, + 0xd4, 0xe1, 0x54, 0x55, 0xb8, 0xd9, 0xd6, 0x70, 0xaa, 0xdb, 0xe2, 0x54, 0x73, 0x59, 0xa7, 0x7d, + 0xd9, 0x3b, 0xe0, 0x10, 0x39, 0x2b, 0x95, 0xfd, 0xde, 0xc8, 0x9a, 0xc2, 0x7a, 0x57, 0x70, 0x0e, + 0xd7, 0xb6, 0x4e, 0x6c, 0x4e, 0xb2, 0xb6, 0x4f, 0xda, 0xd0, 0xc5, 0x33, 0xf4, 0x20, 0xa5, 0xcb, + 0x31, 0xc6, 0xb0, 0xc0, 0x99, 0xaa, 0xb7, 0xcb, 0x1b, 0x1c, 0xfc, 0x68, 0x6d, 0xe2, 0xaa, 0xf3, + 0x48, 0xcb, 0xc2, 0x74, 0xb5, 0x12, 0xc9, 0xcc, 0x84, 0xae, 0x21, 0xd5, 0x6d, 0x36, 0x35, 0xa1, + 0x3b, 0xb3, 0x29, 0x61, 0x99, 0x99, 0x0e, 0x76, 0x64, 0xc6, 0x86, 0x30, 0x58, 0xa1, 0xc8, 0x4b, + 0x89, 0x2b, 0x4c, 0x0a, 0x53, 0x82, 0xb6, 0x89, 0xdd, 0x82, 0x7e, 0x21, 0x16, 0x2f, 0x88, 0xe4, + 0xa6, 0x93, 0x85, 0x58, 0x3c, 0xc0, 0x8a, 0xbd, 0x05, 0xde, 0x3c, 0xc2, 0x78, 0xa6, 0x5c, 0xba, + 0x9d, 0xae, 0x32, 0x3c, 0xc0, 0x2a, 0xf8, 0xcd, 0x82, 0xde, 0x04, 0xe5, 0x05, 0xca, 0xd7, 0x12, + 0xb9, 0xf6, 0xe3, 0x61, 0xbf, 0xe2, 0xf1, 0xe8, 0xee, 0x7f, 0x3c, 0x9c, 0xcd, 0xe3, 0x71, 0x13, + 0x9c, 0x89, 0x0c, 0x4f, 0xc7, 0x2a, 0x23, 0x9b, 0x6b, 0x40, 0x6c, 0x1c, 0x85, 0x45, 0x74, 0x81, + 0xe6, 0x45, 0x31, 0x68, 0x47, 0xfb, 0xdc, 0x3d, 0xda, 0xf7, 0x83, 0x05, 0xbd, 0x87, 0xa2, 0x4a, + 0xcb, 0x62, 0x87, 0x85, 0x43, 0x18, 0x8c, 0xb2, 0x2c, 0x8e, 0x42, 0xfd, 0x6b, 0x7d, 0xa3, 0xb6, + 0x89, 0x76, 0x3c, 0x6a, 0xd5, 0x57, 0xdf, 0xad, 0x6d, 0x22, 0xb9, 0x38, 0x51, 0xfa, 0xae, 0xc5, + 0xba, 0x25, 0x17, 0x5a, 0xd6, 0x95, 0x93, 0x8a, 0x30, 0x2a, 0x8b, 0x74, 0x1e, 0xa7, 0x97, 0xea, + 0xb6, 0x2e, 0x6f, 0x70, 0xf0, 0x7b, 0x07, 0xba, 0xff, 0x95, 0x26, 0x1f, 0x80, 0x15, 0x99, 0x66, + 0x5b, 0x51, 0xa3, 0xd0, 0xfd, 0x96, 0x42, 0xfb, 0xd0, 0xaf, 0xa4, 0x48, 0x16, 0x98, 0xfb, 0xae, + 0x52, 0x97, 0x1a, 0x2a, 0x8f, 0x9a, 0x23, 0x2d, 0xcd, 0x1e, 0xaf, 0x61, 0x33, 0x17, 0xd0, 0x9a, + 0x8b, 0x8f, 0x8c, 0x8a, 0x0f, 0x54, 0x46, 0xfe, 0x76, 0x59, 0xae, 0x8a, 0xf7, 0xbf, 0xa7, 0xc9, + 0x7f, 0x59, 0xe0, 0x34, 0x43, 0x75, 0xb2, 0x3d, 0x54, 0x27, 0x9b, 0xa1, 0x1a, 0x1f, 0xd7, 0x43, + 0x35, 0x3e, 0x26, 0xcc, 0xcf, 0xea, 0xa1, 0xe2, 0x67, 0xd4, 0xac, 0xfb, 0x32, 0x2d, 0xb3, 0xe3, + 0x4a, 0x77, 0xd5, 0xe3, 0x0d, 0x26, 0x26, 0x7e, 0xb3, 0x44, 0x69, 0x4a, 0xed, 0x71, 0x83, 0x88, + 0xb7, 0x0f, 0x95, 0xe0, 0xe8, 0xe2, 0x6a, 0xc0, 0xde, 0x03, 0x87, 0x53, 0xf1, 0x54, 0x85, 0xb7, + 0xfa, 0xa2, 0xcc, 0x5c, 0x7b, 0x29, 0xa8, 0xfe, 0x7a, 0x33, 0x04, 0xae, 0xbf, 0xe5, 0x3e, 0x84, + 0xde, 0x64, 0x19, 0xcd, 0x8b, 0xfa, 0x2d, 0xfc, 0x7f, 0x4b, 0xb0, 0xa2, 0x15, 0x2a, 0x1f, 0x37, + 0x5b, 0x82, 0xa7, 0xe0, 0x35, 0xc6, 0x4d, 0x3a, 0x56, 0x3b, 0x1d, 0x06, 0xdd, 0xf3, 0x24, 0x2a, + 0xea, 0xd1, 0xa5, 0x35, 0x5d, 0xf6, 0x69, 0x29, 0x92, 0x22, 0x2a, 0xaa, 0x7a, 0x74, 0x6b, 0x1c, + 0xdc, 0x33, 0xe9, 0x53, 0xb8, 0xf3, 0x2c, 0x43, 0x69, 0x64, 0x40, 0x03, 0x75, 0x48, 0x7a, 0x89, + 0x5a, 0xc1, 0x6d, 0xae, 0x41, 0xf0, 0x2d, 0x78, 0xa3, 0x18, 0x65, 0xc1, 0xcb, 0x18, 0xf7, 0xbd, + 0x8c, 0x5f, 0x4f, 0x9e, 0x3c, 0xae, 0x33, 0xa0, 0xf5, 0x66, 0xe4, 0xed, 0x2b, 0x23, 0xff, 0x40, + 0x64, 0xe2, 0x74, 0xac, 0x78, 0x6e, 0x73, 0x83, 0x82, 0x9f, 0x2c, 0xe8, 0x92, 0xb6, 0xb4, 0x42, + 0x77, 0x5f, 0xa5, 0x4b, 0x67, 0x32, 0xbd, 0x88, 0x66, 0x28, 0xeb, 0xcb, 0xd5, 0x58, 0x15, 0x3d, + 0x5c, 0x62, 0xf3, 0x00, 0x1b, 0x44, 0x5c, 0xa3, 0x4f, 0xbd, 0x7a, 0x96, 0x5a, 0x5c, 0x23, 0x33, + 0xd7, 0x4e, 0xf6, 0x36, 0xc0, 0xa4, 0xcc, 0x50, 0x8e, 0x66, 0xab, 0x28, 0x51, 0x4d, 0x77, 0x79, + 0xcb, 0x12, 0x7c, 0xa1, 0x3f, 0x1e, 0x77, 0x14, 0xca, 0xda, 0xff, 0xa1, 0x79, 0x35, 0xf3, 0x20, + 0xde, 0xfe, 0xdd, 0x6b, 0xdd, 0x76, 0x08, 0x03, 0xf3, 0xa5, 0xad, 0xbe, 0x5b, 0x8d, 0x58, 0xb5, + 0x4c, 0x74, 0xe7, 0xb3, 0x72, 0x1a, 0x47, 0xa1, 0xba, 0xb3, 0xcb, 0x0d, 0x9a, 0xf6, 0xd4, 0x1f, + 0x8a, 0x7b, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xe0, 0xc4, 0x7a, 0x3e, 0x62, 0x0c, 0x00, 0x00, } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index a56dd62a3c..2e540a64a4 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -26,15 +26,24 @@ message Dashboard { } message DashboardCell { - int32 x = 1; // X-coordinate of Cell in the Dashboard - int32 y = 2; // Y-coordinate of Cell in the Dashboard - int32 w = 3; // Width of Cell in the Dashboard - int32 h = 4; // Height of Cell in the Dashboard - repeated Query queries = 5; // Time-series data queries for Dashboard - string name = 6; // User-facing name for this Dashboard - string type = 7; // Dashboard visualization type - string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6 - map axes = 9; // Axes represent the graphical viewport for a cell's visualizations + int32 x = 1; // X-coordinate of Cell in the Dashboard + int32 y = 2; // Y-coordinate of Cell in the Dashboard + int32 w = 3; // Width of Cell in the Dashboard + int32 h = 4; // Height of Cell in the Dashboard + repeated Query queries = 5; // Time-series data queries for Dashboard + string name = 6; // User-facing name for this Dashboard + string type = 7; // Dashboard visualization type + string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6 + map axes = 9; // Axes represent the graphical viewport for a cell's visualizations + repeated Color colors = 10; // Colors represent encoding data values to color +} + +message Color { + string ID = 1; // ID is the unique id of the cell color + string Type = 2; // Type is how the color is used. Accepted (min,max,threshold) + string Hex = 3; // Hex is the hex number of the color + string Name = 4; // Name is the user-facing name of the hex color + string Value = 5; // Value is the data value mapped to this color } message Axis { diff --git a/bolt/internal/internal_test.go b/bolt/internal/internal_test.go index b94ea5c7ee..f659d6c965 100644 --- a/bolt/internal/internal_test.go +++ b/bolt/internal/internal_test.go @@ -177,6 +177,22 @@ func Test_MarshalDashboard(t *testing.T) { }, }, Type: "line", + CellColors: []chronograf.CellColor{ + { + ID: "myid", + Type: "min", + Hex: "#234567", + Name: "Laser", + Value: "0", + }, + { + ID: "id2", + Type: "max", + Hex: "#876543", + Name: "Solitude", + Value: "100", + }, + }, }, }, Templates: []chronograf.Template{}, @@ -219,6 +235,22 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { LegacyBounds: [2]int64{0, 5}, }, }, + CellColors: []chronograf.CellColor{ + { + ID: "myid", + Type: "min", + Hex: "#234567", + Name: "Laser", + Value: "0", + }, + { + ID: "id2", + Type: "max", + Hex: "#876543", + Name: "Solitude", + Value: "100", + }, + }, Type: "line", }, }, @@ -253,6 +285,22 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { Scale: "linear", }, }, + CellColors: []chronograf.CellColor{ + { + ID: "myid", + Type: "min", + Hex: "#234567", + Name: "Laser", + Value: "0", + }, + { + ID: "id2", + Type: "max", + Hex: "#876543", + Name: "Solitude", + Value: "100", + }, + }, Type: "line", }, }, @@ -296,6 +344,22 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) { LegacyBounds: [2]int64{}, }, }, + CellColors: []chronograf.CellColor{ + { + ID: "myid", + Type: "min", + Hex: "#234567", + Name: "Laser", + Value: "0", + }, + { + ID: "id2", + Type: "max", + Hex: "#876543", + Name: "Solitude", + Value: "100", + }, + }, Type: "line", }, }, @@ -330,6 +394,22 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) { Scale: "linear", }, }, + CellColors: []chronograf.CellColor{ + { + ID: "myid", + Type: "min", + Hex: "#234567", + Name: "Laser", + Value: "0", + }, + { + ID: "id2", + Type: "max", + Hex: "#876543", + Name: "Solitude", + Value: "100", + }, + }, Type: "line", }, }, diff --git a/chronograf.go b/chronograf.go index 9e3824e9e6..77c7f6f896 100644 --- a/chronograf.go +++ b/chronograf.go @@ -15,13 +15,15 @@ const ( ErrLayoutNotFound = Error("layout not found") ErrDashboardNotFound = Error("dashboard not found") ErrUserNotFound = Error("user not found") - ErrUserAlreadyExists = Error("user already exists") - ErrOrganizationNotFound = Error("organization not found") ErrLayoutInvalid = Error("layout is invalid") ErrAlertNotFound = Error("alert not found") ErrAuthentication = Error("user not authenticated") ErrUninitialized = Error("client uninitialized. Call Open() method") ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") + ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'") + ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB") + ErrUserAlreadyExists = Error("user already exists") + ErrOrganizationNotFound = Error("organization not found") ErrOrganizationAlreadyExists = Error("organization already exists") ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization") ) @@ -484,17 +486,27 @@ type Axis struct { Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear" } +// CellColor represents the encoding of data into visualizations +type CellColor struct { + ID string `json:"id"` // ID is the unique id of the cell color + Type string `json:"type"` // Type is how the color is used. Accepted (min,max,threshold) + Hex string `json:"hex"` // Hex is the hex number of the color + Name string `json:"name"` // Name is the user-facing name of the hex color + Value string `json:"value"` // Value is the data value mapped to this color +} + // 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"` + 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"` } // DashboardsStore is the storage and retrieval of dashboards diff --git a/server/cells.go b/server/cells.go index 344f99b642..e1d0c08fa4 100644 --- a/server/cells.go +++ b/server/cells.go @@ -34,6 +34,9 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries)) copy(newCell.Queries, cell.Queries) + newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors)) + copy(newCell.CellColors, cell.CellColors) + // ensure x, y, and y2 axes always returned labels := []string{"x", "y", "y2"} newCell.Axes = make(map[string]chronograf.Axis, len(labels)) @@ -80,7 +83,11 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { } } MoveTimeShift(c) - return HasCorrectAxes(c) + err := HasCorrectAxes(c) + if err != nil { + return err + } + return HasCorrectColors(c) } // HasCorrectAxes verifies that only permitted axes exist within a DashboardCell @@ -102,6 +109,19 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error { return nil } +// HasCorrectColors verifies that the format of each color is correct +func HasCorrectColors(c *chronograf.DashboardCell) error { + for _, color := range c.CellColors { + if !oneOf(color.Type, "max", "min", "threshold") { + return chronograf.ErrInvalidColorType + } + if len(color.Hex) != 7 { + return chronograf.ErrInvalidColor + } + } + return nil +} + // oneOf reports whether a provided string is a member of a variadic list of // valid options func oneOf(prop string, validOpts ...string) bool { diff --git a/server/cells_test.go b/server/cells_test.go index 546a0902c8..e90014af19 100644 --- a/server/cells_test.go +++ b/server/cells_test.go @@ -25,8 +25,8 @@ func Test_Cells_CorrectAxis(t *testing.T) { shouldFail bool }{ { - "correct axes", - &chronograf.DashboardCell{ + name: "correct axes", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{"0", "100"}, @@ -39,11 +39,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid axes present", - &chronograf.DashboardCell{ + name: "invalid axes present", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "axis of evil": chronograf.Axis{ Bounds: []string{"666", "666"}, @@ -53,11 +52,11 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, { - "linear scale value", - &chronograf.DashboardCell{ + name: "linear scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "linear", @@ -65,11 +64,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "log scale value", - &chronograf.DashboardCell{ + name: "log scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "log", @@ -77,11 +75,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid scale value", - &chronograf.DashboardCell{ + name: "invalid scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "potatoes", @@ -89,11 +86,11 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, { - "base 10 axis", - &chronograf.DashboardCell{ + name: "base 10 axis", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "10", @@ -101,11 +98,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "base 2 axis", - &chronograf.DashboardCell{ + name: "base 2 axis", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "2", @@ -113,11 +109,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid base", - &chronograf.DashboardCell{ + name: "invalid base", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "all your base are belong to us", @@ -125,7 +120,7 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, } @@ -150,16 +145,16 @@ func Test_Service_DashboardCells(t *testing.T) { expectedCode int }{ { - "happy path", - &url.URL{ + name: "happy path", + reqURL: &url.URL{ Path: "/chronograf/v1/dashboards/1/cells", }, - map[string]string{ + ctxParams: map[string]string{ "id": "1", }, - []chronograf.DashboardCell{}, - []chronograf.DashboardCell{}, - http.StatusOK, + mockResponse: []chronograf.DashboardCell{}, + expected: []chronograf.DashboardCell{}, + expectedCode: http.StatusOK, }, { name: "cell axes should always be \"x\", \"y\", and \"y2\"", @@ -184,14 +179,15 @@ func Test_Service_DashboardCells(t *testing.T) { }, expected: []chronograf.DashboardCell{ { - ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "CPU", - Type: "bar", - Queries: []chronograf.DashboardQuery{}, + ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "CPU", + Type: "bar", + Queries: []chronograf.DashboardQuery{}, + CellColors: []chronograf.CellColor{}, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{}, @@ -280,3 +276,76 @@ func Test_Service_DashboardCells(t *testing.T) { }) } } + +func TestHasCorrectColors(t *testing.T) { + tests := []struct { + name string + c *chronograf.DashboardCell + wantErr bool + }{ + { + name: "min type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "min", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "max type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "max", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "threshold type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "threshold", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "invalid color type", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "unknown", + Hex: "#FFFFFF", + }, + }, + }, + wantErr: true, + }, + { + name: "invalid color hex", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "min", + Hex: "bad", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := server.HasCorrectColors(tt.c); (err != nil) != tt.wantErr { + t.Errorf("HasCorrectColors() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/dashboards_test.go b/server/dashboards_test.go index a33a6d856d..e19e1e86f7 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -289,6 +289,7 @@ func Test_newDashboardResponse(t *testing.T) { }, }, }, + CellColors: []chronograf.CellColor{}, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{"0", "100"}, @@ -322,6 +323,7 @@ func Test_newDashboardResponse(t *testing.T) { Bounds: []string{}, }, }, + CellColors: []chronograf.CellColor{}, Queries: []chronograf.DashboardQuery{ { Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", diff --git a/server/swagger.json b/server/swagger.json index b8a1c34e75..0ce6510424 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3956,6 +3956,14 @@ ], "default": "line" }, + "colors": { + "description": + "Colors define encoding data into a visualization", + "type": "array", + "items": { + "$ref": "#/definitions/DashboardColor" + } + }, "links": { "type": "object", "properties": { @@ -4026,6 +4034,36 @@ } } }, + "DashboardColor": { + "type": "object", + "description": + "Color defines an encoding of a data value into color space", + "properties": { + "id": { + "description": "ID is the unique id of the cell color", + "type": "string" + }, + "type": { + "description": "Type is how the color is used.", + "type": "string", + "enum": ["min", "max", "threshold"] + }, + "hex": { + "description": "Hex is the hex number of the color", + "type": "string", + "maxLength": 7, + "minLength": 7 + }, + "name": { + "description": "Name is the user-facing name of the hex color", + "type": "string" + }, + "value": { + "description": "Value is the data value mapped to this color", + "type": "string" + } + } + }, "Axis": { "type": "object", "description": "A description of a particular axis for a visualization", diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.js index c813889b9d..5b6bf832fa 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.js @@ -5,7 +5,7 @@ import classnames from 'classnames' import {Link} from 'react-router' import uuid from 'node-uuid' -import FancyScrollbar from 'shared/components/FancyScrollbar' +import InfiniteScroll from 'shared/components/InfiniteScroll' import {ALERTS_TABLE} from 'src/alerts/constants/tableSizing' @@ -49,7 +49,7 @@ class AlertsTable extends Component { } } - sortableClasses = key => () => { + sortableClasses = key => { if (this.state.sortKey === key) { if (this.state.sortDirection === 'asc') { return 'alert-history-table--th sortable-header sorting-ascending' @@ -117,11 +117,10 @@ class AlertsTable extends Component { Value - - {alerts.map(({name, level, time, host, value}) => { + itemHeight={25} + items={alerts.map(({name, level, time, host, value}) => { return (
) })} - + />
: this.renderTableEmpty() } diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index fcce69a055..8cd5849e79 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -3,7 +3,10 @@ import React, {PropTypes} from 'react' import OptIn from 'shared/components/OptIn' 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 {GRAPH_TYPES} from 'src/dashboards/graphics/graph' const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS const getInputMin = scale => (scale === LOG ? '0' : null) @@ -16,86 +19,98 @@ const AxesOptions = ({ onSetPrefixSuffix, onSetYAxisBoundMin, onSetYAxisBoundMax, + selectedGraphType, }) => { const [min, max] = bounds + const {menuOption} = GRAPH_TYPES.find( + graph => graph.type === selectedGraphType + ) + return ( -
-
Y Axis Controls
-
-
- - +
+
+ {menuOption} Controls +
+ +
+ + +
+
+ + +
+
+ + +
+ -
-
- - -
-
- - -
- - - - - - - - - - - -
+ + + + + + + + + +
+ ) } @@ -115,6 +130,7 @@ AxesOptions.defaultProps = { } AxesOptions.propTypes = { + selectedGraphType: string.isRequired, onSetPrefixSuffix: func.isRequired, onSetYAxisBoundMin: func.isRequired, onSetYAxisBoundMax: func.isRequired, diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 59e8d193d4..87beea4297 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -22,12 +22,21 @@ import { import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import {AUTO_GROUP_BY} from 'shared/constants' +import { + COLOR_TYPE_THRESHOLD, + MAX_THRESHOLDS, + DEFAULT_COLORS, + GAUGE_COLORS, + COLOR_TYPE_MIN, + COLOR_TYPE_MAX, + validateColors, +} from 'src/dashboards/constants/gaugeColors' class CellEditorOverlay extends Component { constructor(props) { super(props) - const {cell: {name, type, queries, axes}, sources} = props + const {cell: {name, type, queries, axes, colors}, sources} = props let source = _.get(queries, ['0', 'source'], null) source = sources.find(s => s.links.self === source) || props.source @@ -47,6 +56,7 @@ class CellEditorOverlay extends Component { activeQueryIndex: 0, isDisplayOptionsTabActive: false, axes, + colors: validateColors(colors) ? colors : DEFAULT_COLORS, } } @@ -63,6 +73,111 @@ class CellEditorOverlay extends Component { } } + handleAddThreshold = () => { + const {colors} = this.state + + if (colors.length <= MAX_THRESHOLDS) { + const randomColor = _.random(0, GAUGE_COLORS.length) + + const maxValue = Number( + colors.find(color => color.type === COLOR_TYPE_MAX).value + ) + const minValue = Number( + colors.find(color => color.type === COLOR_TYPE_MIN).value + ) + + const colorsValues = _.mapValues(colors, 'value') + let randomValue + + do { + randomValue = `${_.round(_.random(minValue, maxValue, true), 2)}` + } while (_.includes(colorsValues, randomValue)) + + const newThreshold = { + type: COLOR_TYPE_THRESHOLD, + id: uuid.v4(), + value: randomValue, + hex: GAUGE_COLORS[randomColor].hex, + name: GAUGE_COLORS[randomColor].name, + } + + this.setState({colors: [...colors, newThreshold]}) + } + } + + handleDeleteThreshold = threshold => () => { + const {colors} = this.state + + const newColors = colors.filter(color => color.id !== threshold.id) + + this.setState({colors: newColors}) + } + + handleChooseColor = threshold => chosenColor => { + const {colors} = this.state + + const newColors = colors.map( + color => + color.id === threshold.id + ? {...color, hex: chosenColor.hex, name: chosenColor.name} + : color + ) + + this.setState({colors: newColors}) + } + + handleUpdateColorValue = (threshold, newValue) => { + const {colors} = this.state + const newColors = colors.map( + color => (color.id === threshold.id ? {...color, value: newValue} : color) + ) + this.setState({colors: newColors}) + } + + handleValidateColorValue = (threshold, e) => { + const {colors} = this.state + const sortedColors = _.sortBy(colors, color => Number(color.value)) + const targetValueNumber = Number(e.target.value) + + const maxValue = Number( + colors.find(color => color.type === COLOR_TYPE_MAX).value + ) + const minValue = Number( + colors.find(color => color.type === COLOR_TYPE_MIN).value + ) + + let allowedToUpdate = false + + // If type === min, make sure it is less than the next threshold + if (threshold.type === COLOR_TYPE_MIN) { + const nextValue = Number(sortedColors[1].value) + allowedToUpdate = targetValueNumber < nextValue && targetValueNumber >= 0 + } + // If type === max, make sure it is greater than the previous threshold + if (threshold.type === COLOR_TYPE_MAX) { + const previousValue = Number(sortedColors[sortedColors.length - 2].value) + allowedToUpdate = previousValue < targetValueNumber + } + // If type === threshold, make sure new value is greater than min, less than max, and unique + if (threshold.type === COLOR_TYPE_THRESHOLD) { + const greaterThanMin = targetValueNumber > minValue + const lessThanMax = targetValueNumber < maxValue + + const colorsWithoutMinOrMax = sortedColors.slice( + 1, + sortedColors.length - 1 + ) + + const isUnique = !colorsWithoutMinOrMax.some( + color => color.value === e.target.value + ) + + allowedToUpdate = greaterThanMin && lessThanMax && isUnique + } + + return allowedToUpdate + } + queryStateReducer = queryModifier => (queryID, ...payload) => { const {queriesWorkingDraft} = this.state const query = queriesWorkingDraft.find(q => q.id === queryID) @@ -145,6 +260,7 @@ class CellEditorOverlay extends Component { cellWorkingType: type, cellWorkingName: name, axes, + colors, } = this.state const {cell} = this.props @@ -166,6 +282,7 @@ class CellEditorOverlay extends Component { type, queries, axes, + colors, }) } @@ -296,6 +413,7 @@ class CellEditorOverlay extends Component { const { axes, + colors, activeQueryIndex, cellWorkingName, cellWorkingType, @@ -323,6 +441,7 @@ class CellEditorOverlay extends Component { > - + {isGauge + ? + : }
) } @@ -66,6 +85,11 @@ class DisplayOptions extends Component { const {arrayOf, func, shape, string} = PropTypes DisplayOptions.propTypes = { + onAddThreshold: func.isRequired, + onDeleteThreshold: func.isRequired, + onChooseColor: func.isRequired, + onValidateColorValue: func.isRequired, + onUpdateColorValue: func.isRequired, selectedGraphType: string.isRequired, onSelectGraphType: func.isRequired, onSetPrefixSuffix: func.isRequired, @@ -75,6 +99,15 @@ DisplayOptions.propTypes = { onSetLabel: func.isRequired, onSetBase: func.isRequired, axes: shape({}).isRequired, + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), queryConfigs: arrayOf(shape()).isRequired, } diff --git a/ui/src/dashboards/components/DisplayOptionsInput.js b/ui/src/dashboards/components/DisplayOptionsInput.js index f0f5a61b46..67e9e49cda 100644 --- a/ui/src/dashboards/components/DisplayOptionsInput.js +++ b/ui/src/dashboards/components/DisplayOptionsInput.js @@ -1,7 +1,15 @@ import React, {PropTypes} from 'react' -const DisplayOptionsInput = ({id, name, value, onChange, labelText}) => -
+const DisplayOptionsInput = ({ + id, + name, + value, + onChange, + labelText, + colWidth, + placeholder, +}) => +
@@ -12,6 +20,7 @@ const DisplayOptionsInput = ({id, name, value, onChange, labelText}) => id={id} value={value} onChange={onChange} + placeholder={placeholder} />
@@ -19,6 +28,8 @@ const {func, string} = PropTypes DisplayOptionsInput.defaultProps = { value: '', + colWidth: 'col-sm-6', + placeholder: '', } DisplayOptionsInput.propTypes = { @@ -27,6 +38,8 @@ DisplayOptionsInput.propTypes = { value: string.isRequired, onChange: func.isRequired, labelText: string, + colWidth: string, + placeholder: string, } export default DisplayOptionsInput diff --git a/ui/src/dashboards/components/GaugeOptions.js b/ui/src/dashboards/components/GaugeOptions.js new file mode 100644 index 0000000000..297dec7e37 --- /dev/null +++ b/ui/src/dashboards/components/GaugeOptions.js @@ -0,0 +1,82 @@ +import React, {PropTypes} from 'react' +import _ from 'lodash' + +import FancyScrollbar from 'shared/components/FancyScrollbar' +import GaugeThreshold from 'src/dashboards/components/GaugeThreshold' + +import { + MAX_THRESHOLDS, + MIN_THRESHOLDS, + DEFAULT_COLORS, +} from 'src/dashboards/constants/gaugeColors' + +const GaugeOptions = ({ + colors, + onAddThreshold, + onDeleteThreshold, + onChooseColor, + onValidateColorValue, + onUpdateColorValue, +}) => { + const disableMaxColor = colors.length > MIN_THRESHOLDS + + const disableAddThreshold = colors.length > MAX_THRESHOLDS + + const sortedColors = _.sortBy(colors, color => Number(color.value)) + + return ( + +
+
Gauge Controls
+
+ {sortedColors.map(color => + + )} + +
+
+
+ ) +} + +const {arrayOf, func, shape, string} = PropTypes + +GaugeOptions.defaultProps = { + colors: DEFAULT_COLORS, +} + +GaugeOptions.propTypes = { + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), + onAddThreshold: func.isRequired, + onDeleteThreshold: func.isRequired, + onChooseColor: func.isRequired, + onValidateColorValue: func.isRequired, + onUpdateColorValue: func.isRequired, +} + +export default GaugeOptions diff --git a/ui/src/dashboards/components/GaugeThreshold.js b/ui/src/dashboards/components/GaugeThreshold.js new file mode 100644 index 0000000000..19b9246df0 --- /dev/null +++ b/ui/src/dashboards/components/GaugeThreshold.js @@ -0,0 +1,116 @@ +import React, {Component, PropTypes} from 'react' + +import ColorDropdown from 'shared/components/ColorDropdown' + +import { + COLOR_TYPE_MIN, + COLOR_TYPE_MAX, + GAUGE_COLORS, +} from 'src/dashboards/constants/gaugeColors' + +class GaugeThreshold extends Component { + constructor(props) { + super(props) + + this.state = { + workingValue: this.props.threshold.value, + valid: true, + } + } + + handleChangeWorkingValue = e => { + const {threshold, onValidateColorValue, onUpdateColorValue} = this.props + + const valid = onValidateColorValue(threshold, e) + + if (valid) { + onUpdateColorValue(threshold, e.target.value) + } + + this.setState({valid, workingValue: e.target.value}) + } + + handleBlur = () => { + this.setState({workingValue: this.props.threshold.value, valid: true}) + } + + render() { + const { + threshold, + threshold: {type, hex, name}, + disableMaxColor, + onChooseColor, + onDeleteThreshold, + } = this.props + const {workingValue, valid} = this.state + const selectedColor = {hex, name} + + const labelClass = + type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX + ? 'gauge-controls--label' + : 'gauge-controls--label-editable' + + const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX) + + let label = 'Threshold' + if (type === COLOR_TYPE_MIN) { + label = 'Minimum' + } + if (type === COLOR_TYPE_MAX) { + label = 'Maximum' + } + + const inputClass = valid + ? 'form-control input-sm gauge-controls--input' + : 'form-control input-sm gauge-controls--input form-volcano' + + return ( +
+
+ {label} +
+ {canBeDeleted + ? + : null} + + +
+ ) + } +} + +const {bool, func, shape, string} = PropTypes + +GaugeThreshold.propTypes = { + threshold: shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired, + disableMaxColor: bool, + onChooseColor: func.isRequired, + onValidateColorValue: func.isRequired, + onUpdateColorValue: func.isRequired, + onDeleteThreshold: func.isRequired, +} + +export default GaugeThreshold diff --git a/ui/src/dashboards/components/GraphTypeSelector.js b/ui/src/dashboards/components/GraphTypeSelector.js index 529ae2eff3..a27f41ac45 100644 --- a/ui/src/dashboards/components/GraphTypeSelector.js +++ b/ui/src/dashboards/components/GraphTypeSelector.js @@ -1,29 +1,35 @@ import React, {PropTypes} from 'react' import classnames from 'classnames' +import FancyScrollbar from 'shared/components/FancyScrollbar' -import {graphTypes} from 'src/dashboards/graphics/graph' +import {GRAPH_TYPES} from 'src/dashboards/graphics/graph' const GraphTypeSelector = ({selectedGraphType, onSelectGraphType}) => -
-
Visualization Type
-
- {graphTypes.map(graphType => -
-
- {graphType.graphic} -

- {graphType.menuOption} -

+ +
+
Visualization Type
+
+ {GRAPH_TYPES.map(graphType => +
+
+ {graphType.graphic} +

+ {graphType.menuOption} +

+
-
- )} + )} +
-
+ const {func, string} = PropTypes diff --git a/ui/src/dashboards/components/OverlayControls.js b/ui/src/dashboards/components/OverlayControls.js index 93a49b4e93..fc60343264 100644 --- a/ui/src/dashboards/components/OverlayControls.js +++ b/ui/src/dashboards/components/OverlayControls.js @@ -39,7 +39,7 @@ const OverlayControls = ({ })} onClick={onClickDisplayOptions(true)} > - Options + Visualization
diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 0197bbec88..2d6764d708 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -8,12 +8,14 @@ const DashVisualization = ( axes, type, name, + colors, templates, timeRange, autoRefresh, onCellRename, queryConfigs, editQueryStatus, + resizerTopHeight, }, {source: {links: {proxy}}} ) => @@ -21,12 +23,14 @@ const DashVisualization = (
@@ -55,6 +59,16 @@ DashVisualization.propTypes = { }), }), onCellRename: func, + resizerTopHeight: number, + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), } DashVisualization.contextTypes = { diff --git a/ui/src/dashboards/constants/gaugeColors.js b/ui/src/dashboards/constants/gaugeColors.js new file mode 100644 index 0000000000..bc0416f8ae --- /dev/null +++ b/ui/src/dashboards/constants/gaugeColors.js @@ -0,0 +1,106 @@ +export const MAX_THRESHOLDS = 5 +export const MIN_THRESHOLDS = 2 + +export const COLOR_TYPE_MIN = 'min' +export const DEFAULT_VALUE_MIN = '0' +export const COLOR_TYPE_MAX = 'max' +export const DEFAULT_VALUE_MAX = '100' +export const COLOR_TYPE_THRESHOLD = 'threshold' + +export const GAUGE_COLORS = [ + { + hex: '#BF3D5E', + name: 'ruby', + }, + { + hex: '#DC4E58', + name: 'fire', + }, + { + hex: '#F95F53', + name: 'curacao', + }, + { + hex: '#F48D38', + name: 'tiger', + }, + { + hex: '#FFB94A', + name: 'pineapple', + }, + { + hex: '#FFD255', + name: 'thunder', + }, + { + hex: '#7CE490', + name: 'honeydew', + }, + { + hex: '#4ED8A0', + name: 'rainforest', + }, + { + hex: '#32B08C', + name: 'viridian', + }, + { + hex: '#4591ED', + name: 'ocean', + }, + { + hex: '#22ADF6', + name: 'pool', + }, + { + hex: '#00C9FF', + name: 'laser', + }, + { + hex: '#513CC6', + name: 'planet', + }, + { + hex: '#7A65F2', + name: 'star', + }, + { + hex: '#9394FF', + name: 'comet', + }, + { + hex: '#383846', + name: 'pepper', + }, + { + hex: '#545667', + name: 'graphite', + }, +] + +export const DEFAULT_COLORS = [ + { + type: COLOR_TYPE_MIN, + hex: GAUGE_COLORS[11].hex, + id: '0', + name: GAUGE_COLORS[11].name, + value: DEFAULT_VALUE_MIN, + }, + { + type: COLOR_TYPE_MAX, + hex: GAUGE_COLORS[14].hex, + id: '1', + name: GAUGE_COLORS[14].name, + value: DEFAULT_VALUE_MAX, + }, +] + +export const validateColors = colors => { + if (!colors) { + return false + } + const hasMin = colors.some(color => color.type === COLOR_TYPE_MIN) + const hasMax = colors.some(color => color.type === COLOR_TYPE_MAX) + + return hasMin && hasMax +} diff --git a/ui/src/dashboards/graphics/graph.js b/ui/src/dashboards/graphics/graph.js index 7a238aa93e..6fb9a6cc19 100644 --- a/ui/src/dashboards/graphics/graph.js +++ b/ui/src/dashboards/graphics/graph.js @@ -1,9 +1,9 @@ import React from 'react' -export const graphTypes = [ +export const GRAPH_TYPES = [ { type: 'line', - menuOption: 'Line', + menuOption: 'Line Graph', graphic: (
- +
@@ -46,7 +46,7 @@ export const graphTypes = [ }, { type: 'line-stacked', - menuOption: 'Stacked', + menuOption: 'Stacked Graph', graphic: (
- - - + + +
@@ -89,7 +89,7 @@ export const graphTypes = [ }, { type: 'line-stepplot', - menuOption: 'Step-Plot', + menuOption: 'Step-Plot Graph', graphic: (
+ +
@@ -124,7 +132,7 @@ export const graphTypes = [ }, { type: 'single-stat', - menuOption: 'SingleStat', + menuOption: 'Single Stat', graphic: (
- + + +
@@ -159,7 +175,7 @@ export const graphTypes = [ }, { type: 'line-plus-single-stat', - menuOption: 'Line + Stat', + menuOption: 'Line Graph + Single Stat', graphic: (
- - - - + + + + - + +
@@ -210,7 +228,7 @@ export const graphTypes = [ }, { type: 'bar', - menuOption: 'Bar', + menuOption: 'Bar Graph', graphic: (
- + + + + + - - + + + + +
+ ), + }, + { + type: 'gauge', + menuOption: 'Gauge', + graphic: ( +
+ + + + + + + + + + + + + + + + + + + + - - - - -
diff --git a/ui/src/hosts/components/HostRow.js b/ui/src/hosts/components/HostRow.js index 16a7d42916..159decb398 100644 --- a/ui/src/hosts/components/HostRow.js +++ b/ui/src/hosts/components/HostRow.js @@ -20,13 +20,13 @@ class HostRow extends Component { const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE return ( - - +
+
{name} - - +
+
- - +
+
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`} - - +
+
{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`} - - +
+
{apps.map((app, index) => { return ( @@ -59,8 +59,8 @@ class HostRow extends Component { ) })} - - +
+
) } } diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index 3873da99a4..585f8877b3 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -3,6 +3,7 @@ import _ from 'lodash' import SearchBar from 'src/hosts/components/SearchBar' import HostRow from 'src/hosts/components/HostRow' +import InfiniteScroll from 'shared/components/InfiniteScroll' import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing' @@ -67,11 +68,11 @@ class HostsTable extends Component { sortableClasses = key => { if (this.state.sortKey === key) { if (this.state.sortDirection === 'asc') { - return 'sortable-header sorting-ascending' + return 'hosts-table--th sortable-header sorting-ascending' } - return 'sortable-header sorting-descending' + return 'hosts-table--th sortable-header sorting-descending' } - return 'sortable-header' + return 'hosts-table--th sortable-header' } render() { @@ -110,47 +111,48 @@ class HostsTable extends Component {
{hostCount > 0 && !hostsError.length - ? - - - - - - - - - - - - {sortedHosts.map(h => + +
Apps
+ + + )} - -
+
+
+
Host -
+
Status -
+
CPU -
+
Load -
Apps
+ itemHeight={26} + className="hosts-table--tbody" + /> +
:

No Hosts found

} diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js index d93358bc99..b08e324448 100644 --- a/ui/src/hosts/containers/HostsPage.js +++ b/ui/src/hosts/containers/HostsPage.js @@ -2,7 +2,6 @@ import React, {PropTypes, Component} from 'react' import _ from 'lodash' import HostsTable from 'src/hosts/components/HostsTable' -import FancyScrollbar from 'shared/components/FancyScrollbar' import SourceIndicator from 'shared/components/SourceIndicator' import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis' @@ -66,7 +65,7 @@ class HostsPage extends Component { const {source} = this.props const {hosts, hostsLoading, hostsError} = this.state return ( -
+
@@ -77,7 +76,7 @@ class HostsPage extends Component {
- +
@@ -90,7 +89,7 @@ class HostsPage extends Component {
- +
) } diff --git a/ui/src/kapacitor/components/LogsTable.js b/ui/src/kapacitor/components/LogsTable.js index 0e49badd36..ad7945c1b0 100644 --- a/ui/src/kapacitor/components/LogsTable.js +++ b/ui/src/kapacitor/components/LogsTable.js @@ -1,6 +1,6 @@ import React, {PropTypes} from 'react' -import FancyScrollbar from 'shared/components/FancyScrollbar' +import InfiniteScroll from 'shared/components/InfiniteScroll' import LogsTableRow from 'src/kapacitor/components/LogsTableRow' const LogsTable = ({logs}) => @@ -8,18 +8,17 @@ const LogsTable = ({logs}) =>

Logs

- -
- {logs.length - ? logs.map((log, i) => +
+ {logs.length + ? - ) - :
} -
- + )} + /> + :
} +
const {arrayOf, shape, string} = PropTypes diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js index 8abf937aa2..b73a11edf3 100644 --- a/ui/src/kapacitor/containers/TickscriptPage.js +++ b/ui/src/kapacitor/containers/TickscriptPage.js @@ -44,7 +44,8 @@ class TickscriptPage extends Component { }) notify( 'warning', - 'Could not use logging, requires Kapacitor version 1.4' + 'Could not use logging, requires Kapacitor version 1.4', + {once: true} ) return } @@ -101,13 +102,13 @@ class TickscriptPage extends Component { } this.setState({ - logs: [...this.state.logs, ...logs], + logs: [...logs, ...this.state.logs], failStr, }) } catch (err) { console.warn(err, failStr) this.setState({ - logs: [...this.state.logs, ...logs], + logs: [...logs, ...this.state.logs], failStr, }) } diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 77cb2268f2..55c7aff557 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -52,6 +52,7 @@ export const saveToLocalStorage = ({ timeRange, dataExplorer, dashTimeV1: {ranges}, + dismissedNotifications, }) => { try { const appPersisted = Object.assign({}, {app: {persisted}}) @@ -66,6 +67,7 @@ export const saveToLocalStorage = ({ dataExplorer, VERSION, // eslint-disable-line no-undef dashTimeV1, + dismissedNotifications, }) ) } catch (err) { diff --git a/ui/src/shared/actions/notifications.js b/ui/src/shared/actions/notifications.js index deab1e28bb..23c830ce6d 100644 --- a/ui/src/shared/actions/notifications.js +++ b/ui/src/shared/actions/notifications.js @@ -1,4 +1,4 @@ -export function publishNotification(type, message) { +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) { @@ -10,6 +10,7 @@ export function publishNotification(type, message) { payload: { type, message, + once: options.once, }, } } diff --git a/ui/src/shared/components/ColorDropdown.js b/ui/src/shared/components/ColorDropdown.js new file mode 100644 index 0000000000..b91058df64 --- /dev/null +++ b/ui/src/shared/components/ColorDropdown.js @@ -0,0 +1,109 @@ +import React, {Component, PropTypes} from 'react' + +import classnames from 'classnames' +import OnClickOutside from 'shared/components/OnClickOutside' +import FancyScrollbar from 'shared/components/FancyScrollbar' + +class ColorDropdown extends Component { + constructor(props) { + super(props) + + this.state = { + visible: false, + } + } + + handleToggleMenu = () => { + const {disabled} = this.props + + if (disabled) { + return + } + this.setState({visible: !this.state.visible}) + } + + handleClickOutside = () => { + this.setState({visible: false}) + } + + handleColorClick = color => () => { + this.props.onChoose(color) + this.setState({visible: false}) + } + + render() { + const {visible} = this.state + const {colors, selected, disabled} = this.props + + const dropdownClassNames = visible + ? 'color-dropdown open' + : 'color-dropdown' + const toggleClassNames = classnames( + 'btn btn-sm btn-default color-dropdown--toggle', + {active: visible, 'color-dropdown__disabled': disabled} + ) + + return ( +
+
+
+
+ {selected.name} +
+ +
+ {visible + ?
+ + {colors.map((color, i) => +
+ + + {color.name} + +
+ )} +
+
+ : null} +
+ ) + } +} + +const {arrayOf, bool, func, shape, string} = PropTypes + +ColorDropdown.propTypes = { + selected: shape({ + hex: string.isRequired, + name: string.isRequired, + }).isRequired, + onChoose: func.isRequired, + colors: arrayOf( + shape({ + hex: string.isRequired, + name: string.isRequired, + }) + ).isRequired, + disabled: bool, +} + +export default OnClickOutside(ColorDropdown) diff --git a/ui/src/shared/components/Gauge.js b/ui/src/shared/components/Gauge.js new file mode 100644 index 0000000000..c9adfd521c --- /dev/null +++ b/ui/src/shared/components/Gauge.js @@ -0,0 +1,340 @@ +import React, {Component, PropTypes} from 'react' +import _ from 'lodash' + +import {GAUGE_SPECS} from 'shared/constants/gaugeSpecs' + +import { + COLOR_TYPE_MIN, + COLOR_TYPE_MAX, + MIN_THRESHOLDS, +} from 'src/dashboards/constants/gaugeColors' + +class Gauge extends Component { + constructor(props) { + super(props) + } + + componentDidMount() { + this.updateCanvas() + } + + componentDidUpdate() { + this.updateCanvas() + } + + resetCanvas = (canvas, context) => { + context.setTransform(1, 0, 0, 1, 0, 0) + context.clearRect(0, 0, canvas.width, canvas.height) + } + + updateCanvas = () => { + const canvas = this.canvasRef + canvas.width = canvas.height * (canvas.clientWidth / canvas.clientHeight) + const ctx = canvas.getContext('2d') + + this.resetCanvas(canvas, ctx) + + const centerX = canvas.width / 2 + const centerY = canvas.height / 2 * 1.13 + const radius = Math.min(canvas.width, canvas.height) / 2 * 0.5 + + const {minLineWidth, minFontSize} = GAUGE_SPECS + const gradientThickness = Math.max(minLineWidth, radius / 4) + const labelValueFontSize = Math.max(minFontSize, radius / 4) + + const {colors} = this.props + if (!colors || colors.length === 0) { + return + } + // Distill out max and min values + const minValue = Number( + colors.find(color => color.type === COLOR_TYPE_MIN).value + ) + const maxValue = Number( + colors.find(color => color.type === COLOR_TYPE_MAX).value + ) + + // The following functions must be called in the specified order + if (colors.length === MIN_THRESHOLDS) { + this.drawGradientGauge(ctx, centerX, centerY, radius, gradientThickness) + } else { + this.drawSegmentedGauge( + ctx, + centerX, + centerY, + radius, + minValue, + maxValue, + gradientThickness + ) + } + this.drawGaugeLines(ctx, centerX, centerY, radius, gradientThickness) + this.drawGaugeLabels( + ctx, + centerX, + centerY, + radius, + gradientThickness, + minValue, + maxValue + ) + this.drawGaugeValue(ctx, radius, labelValueFontSize) + this.drawNeedle(ctx, radius, minValue, maxValue) + } + + drawGradientGauge = (ctx, xc, yc, r, gradientThickness) => { + const {colors} = this.props + const sortedColors = _.sortBy(colors, color => Number(color.value)) + + const arcStart = Math.PI * 0.75 + const arcEnd = arcStart + Math.PI * 1.5 + + // Determine coordinates for gradient + const xStart = xc + Math.cos(arcStart) * r + const yStart = yc + Math.sin(arcStart) * r + const xEnd = xc + Math.cos(arcEnd) * r + const yEnd = yc + Math.sin(arcEnd) * r + + const gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd) + gradient.addColorStop(0, sortedColors[0].hex) + gradient.addColorStop(1.0, sortedColors[1].hex) + + ctx.beginPath() + ctx.lineWidth = gradientThickness + ctx.strokeStyle = gradient + ctx.arc(xc, yc, r, arcStart, arcEnd) + ctx.stroke() + } + + drawSegmentedGauge = ( + ctx, + xc, + yc, + r, + minValue, + maxValue, + gradientThickness + ) => { + const {colors} = this.props + const sortedColors = _.sortBy(colors, color => Number(color.value)) + + const trueValueRange = Math.abs(maxValue - minValue) + const totalArcLength = Math.PI * 1.5 + let startingPoint = Math.PI * 0.75 + + // Iterate through colors, draw arc for each + for (let c = 0; c < sortedColors.length - 1; c++) { + // Use this color and the next to determine arc length + const color = sortedColors[c] + const nextColor = sortedColors[c + 1] + + // adjust values by subtracting minValue from them + const adjustedValue = Number(color.value) - minValue + const adjustedNextValue = Number(nextColor.value) - minValue + + const thisArc = Math.abs(adjustedValue - adjustedNextValue) + // Multiply by arcLength to determine this arc's length + const arcLength = totalArcLength * (thisArc / trueValueRange) + // Draw arc + ctx.beginPath() + ctx.lineWidth = gradientThickness + ctx.strokeStyle = color.hex + ctx.arc(xc, yc, r, startingPoint, startingPoint + arcLength) + ctx.stroke() + // Add this arc's length to starting point + startingPoint += arcLength + } + } + + drawGaugeLines = (ctx, xc, yc, radius, gradientThickness) => { + const { + degree, + lineCount, + lineColor, + lineStrokeSmall, + lineStrokeLarge, + tickSizeSmall, + tickSizeLarge, + } = GAUGE_SPECS + + const arcStart = Math.PI * 0.75 + const arcLength = Math.PI * 1.5 + const arcStop = arcStart + arcLength + const lineSmallCount = lineCount * 5 + const startDegree = degree * 135 + const arcLargeIncrement = arcLength / lineCount + const arcSmallIncrement = arcLength / lineSmallCount + + // Semi-circle + const arcRadius = radius + gradientThickness * 0.8 + ctx.beginPath() + ctx.arc(xc, yc, arcRadius, arcStart, arcStop) + ctx.lineWidth = 3 + ctx.lineCap = 'round' + ctx.strokeStyle = lineColor + ctx.stroke() + ctx.closePath() + + // Match center of canvas to center of gauge + ctx.translate(xc, yc) + + // Draw Large ticks + for (let lt = 0; lt <= lineCount; lt++) { + // Rototion before drawing line + ctx.rotate(startDegree) + ctx.rotate(lt * arcLargeIncrement) + // Draw line + ctx.beginPath() + ctx.lineWidth = lineStrokeLarge + ctx.lineCap = 'round' + ctx.strokeStyle = lineColor + ctx.moveTo(arcRadius, 0) + ctx.lineTo(arcRadius + tickSizeLarge, 0) + ctx.stroke() + ctx.closePath() + // Return to starting rotation + ctx.rotate(-lt * arcLargeIncrement) + ctx.rotate(-startDegree) + } + + // Draw Small ticks + for (let lt = 0; lt <= lineSmallCount; lt++) { + // Rototion before drawing line + ctx.rotate(startDegree) + ctx.rotate(lt * arcSmallIncrement) + // Draw line + ctx.beginPath() + ctx.lineWidth = lineStrokeSmall + ctx.lineCap = 'round' + ctx.strokeStyle = lineColor + ctx.moveTo(arcRadius, 0) + ctx.lineTo(arcRadius + tickSizeSmall, 0) + ctx.stroke() + ctx.closePath() + // Return to starting rotation + ctx.rotate(-lt * arcSmallIncrement) + ctx.rotate(-startDegree) + } + } + + drawGaugeLabels = ( + ctx, + xc, + yc, + radius, + gradientThickness, + minValue, + maxValue + ) => { + const {degree, lineCount, labelColor, labelFontSize} = GAUGE_SPECS + + const incrementValue = (maxValue - minValue) / lineCount + + const gaugeValues = [] + for (let g = minValue; g < maxValue; g += incrementValue) { + const roundedValue = Math.round(g * 100) / 100 + gaugeValues.push(roundedValue.toString()) + } + gaugeValues.push((Math.round(maxValue * 100) / 100).toString()) + + const startDegree = degree * 135 + const arcLength = Math.PI * 1.5 + const arcIncrement = arcLength / lineCount + + // Format labels text + ctx.font = `bold ${labelFontSize}px Helvetica` + ctx.fillStyle = labelColor + ctx.textBaseline = 'middle' + ctx.textAlign = 'right' + let labelRadius + + for (let i = 0; i <= lineCount; i++) { + if (i === 3) { + ctx.textAlign = 'center' + labelRadius = radius + gradientThickness + 30 + } else { + labelRadius = radius + gradientThickness + 23 + } + if (i > 3) { + ctx.textAlign = 'left' + } + ctx.rotate(startDegree) + ctx.rotate(i * arcIncrement) + ctx.translate(labelRadius, 0) + ctx.rotate(i * -arcIncrement) + ctx.rotate(-startDegree) + ctx.fillText(gaugeValues[i], 0, 0) + ctx.rotate(startDegree) + ctx.rotate(i * arcIncrement) + ctx.translate(-labelRadius, 0) + ctx.rotate(i * -arcIncrement) + ctx.rotate(-startDegree) + } + } + + drawGaugeValue = (ctx, radius, labelValueFontSize) => { + const {gaugePosition} = this.props + const {valueColor} = GAUGE_SPECS + + ctx.font = `${labelValueFontSize}px Roboto` + ctx.fillStyle = valueColor + ctx.textBaseline = 'middle' + ctx.textAlign = 'center' + + const textY = radius + ctx.fillText(gaugePosition.toString(), 0, textY) + } + + drawNeedle = (ctx, radius, minValue, maxValue) => { + const {gaugePosition} = this.props + const {degree, needleColor0, needleColor1} = GAUGE_SPECS + const arcDistance = Math.PI * 1.5 + + const needleRotation = (gaugePosition - minValue) / (maxValue - minValue) + + const needleGradient = ctx.createLinearGradient(0, -10, 0, radius) + needleGradient.addColorStop(0, needleColor0) + needleGradient.addColorStop(1, needleColor1) + + // Starting position of needle is at minimum + ctx.rotate(degree * 45) + ctx.rotate(arcDistance * needleRotation) + ctx.beginPath() + ctx.fillStyle = needleGradient + ctx.arc(0, 0, 10, 0, Math.PI, true) + ctx.lineTo(0, radius) + ctx.lineTo(10, 0) + ctx.fill() + } + + render() { + const {width, height} = this.props + return ( + (this.canvasRef = r)} + /> + ) + } +} + +const {arrayOf, number, shape, string} = PropTypes + +Gauge.propTypes = { + width: string.isRequired, + height: string.isRequired, + gaugePosition: number.isRequired, + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ).isRequired, +} + +export default Gauge diff --git a/ui/src/shared/components/GaugeChart.js b/ui/src/shared/components/GaugeChart.js new file mode 100644 index 0000000000..bc49f9bf89 --- /dev/null +++ b/ui/src/shared/components/GaugeChart.js @@ -0,0 +1,83 @@ +import React, {PropTypes, PureComponent} from 'react' +import lastValues from 'shared/parsing/lastValues' +import Gauge from 'shared/components/Gauge' + +import {DEFAULT_COLORS} from 'src/dashboards/constants/gaugeColors' +import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants' + +class GaugeChart extends PureComponent { + render() { + const { + data, + cellHeight, + isFetchingInitially, + colors, + resizeCoords, + resizerTopHeight, + } = this.props + + // If data for this graph is being fetched for the first time, show a graph-wide spinner. + if (isFetchingInitially) { + return ( +
+

+

+ ) + } + + const lastValue = lastValues(data)[1] + const precision = 100.0 + const roundedValue = Math.round(+lastValue * precision) / precision + + // When a new height is passed the Gauge component resizes internally + // Passing in a new often ensures the gauge appears sharp + + const initialCellHeight = + cellHeight && (cellHeight * DASHBOARD_LAYOUT_ROW_HEIGHT).toString() + + const resizeCoordsHeight = + resizeCoords && (resizeCoords.h * DASHBOARD_LAYOUT_ROW_HEIGHT).toString() + + const height = (resizeCoordsHeight || + initialCellHeight || + resizerTopHeight || + 300) + .toString() + + return ( +
+ +
+ ) + } +} + +const {arrayOf, bool, number, shape, string} = PropTypes + +GaugeChart.defaultProps = { + colors: DEFAULT_COLORS, +} + +GaugeChart.propTypes = { + data: arrayOf(shape()).isRequired, + isFetchingInitially: bool, + cellHeight: number, + resizerTopHeight: number, + resizeCoords: shape(), + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), +} + +export default GaugeChart diff --git a/ui/src/shared/components/InfiniteScroll.js b/ui/src/shared/components/InfiniteScroll.js new file mode 100644 index 0000000000..9854ef1afa --- /dev/null +++ b/ui/src/shared/components/InfiniteScroll.js @@ -0,0 +1,131 @@ +import React, {Component, PropTypes} from 'react' +import classnames from 'classnames' +import {Scrollbars} from 'react-custom-scrollbars' +import _ from 'lodash' + +const {arrayOf, number, shape, string} = PropTypes + +class InfiniteScroll extends Component { + // Cache values from Scrollbars events that need to be independent of render + // Should not be setState as need not trigger a re-render + scrollbarsScrollTop = 0 + scrollbarsClientHeight = 0 + + state = { + topIndex: 0, + bottomIndex: 0, + topPadding: 0, + bottomPadding: 0, + windowHeight: window.innerHeight, + } + + windowing = (props, state) => { + const {itemHeight, items} = props + const {bottomIndex} = state + + const itemDistance = Math.round(this.scrollbarsScrollTop / itemHeight) + const itemCount = Math.round(this.scrollbarsClientHeight / itemHeight) + 1 + + // If state is the same, do not setState to the same value multiple times. + // Improves performance and prevents errors. + if (bottomIndex === itemDistance + itemCount) { + return + } + + this.setState({ + // Number of items from top + topIndex: itemDistance, + // Number of items that can fit inside the container div + bottomIndex: itemDistance + itemCount, + // Offset list from top + topPadding: itemDistance * itemHeight, + // Provide scrolling room at the bottom of the list + bottomPadding: (items.length - itemDistance - itemCount) * itemHeight, + }) + } + + handleScroll = ({clientHeight, scrollTop}) => { + let shouldUpdate = false + + if ( + (typeof clientHeight !== 'undefined' && + this.scrollbarsClientHeight !== clientHeight) || + (typeof scrollTop !== 'undefined' && + this.scrollbarsScrollTop !== scrollTop) + ) { + shouldUpdate = true + } + + this.scrollbarsClientHeight = clientHeight + this.scrollbarsScrollTop = scrollTop + + if (shouldUpdate) { + this.windowing(this.props, this.state) + } + } + + throttledHandleScroll = _.throttle(this.handleScroll, 100) + + handleResize = () => { + this.setState({windowHeight: window.innerHeight}) + } + + throttledHandleResize = _.throttle(this.handleResize, 100) + + handleMakeDiv = className => props => +
+ + componentDidMount() { + window.addEventListener('resize', this.handleResize, true) + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize, true) + } + + componentWillReceiveProps(nextProps, nextState) { + // Updates values if new items are added + this.windowing(nextProps, nextState) + } + + render() { + const {className, items} = this.props + const { + topIndex, + bottomIndex, + topPadding, + bottomPadding, + windowHeight, + } = this.state + + return ( + +
+ {items.filter((_item, i) => i >= topIndex && i <= bottomIndex)} +
+ + ) + } +} + +InfiniteScroll.propTypes = { + itemHeight: number.isRequired, + items: arrayOf(shape()).isRequired, + className: string, +} + +export default InfiniteScroll diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index e11d2ad653..966596f96b 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -43,7 +43,7 @@ const Layout = ( { host, cell, - cell: {h, axes, type}, + cell: {h, axes, type, colors}, source, sources, onZoom, @@ -75,6 +75,7 @@ const Layout = ( {cell.isWidget ? : ({ +const mapStateToProps = ({notifications, dismissedNotifications}) => ({ notifications, + dismissedNotifications, }) const mapDispatchToProps = dispatch => ({ diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 2320f1b65a..b3f03b4375 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -5,19 +5,23 @@ import {emptyGraphCopy} from 'src/shared/copy/cell' import AutoRefresh from 'shared/components/AutoRefresh' import LineGraph from 'shared/components/LineGraph' import SingleStat from 'shared/components/SingleStat' +import GaugeChart from 'shared/components/GaugeChart' const RefreshingLineGraph = AutoRefresh(LineGraph) const RefreshingSingleStat = AutoRefresh(SingleStat) +const RefreshingGaugeChart = AutoRefresh(GaugeChart) const RefreshingGraph = ({ axes, type, + colors, onZoom, queries, templates, timeRange, cellHeight, autoRefresh, + resizerTopHeight, manualRefresh, // when changed, re-mounts the component synchronizer, resizeCoords, @@ -46,6 +50,21 @@ const RefreshingGraph = ({ ) } + if (type === 'gauge') { + return ( + + ) + } + const displayOptions = { stepPlot: type === 'line-stepplot', stackedGraph: type === 'line-stacked', @@ -83,12 +102,22 @@ RefreshingGraph.propTypes = { synchronizer: func, type: string.isRequired, cellHeight: number, + resizerTopHeight: number, axes: shape(), queries: arrayOf(shape()).isRequired, editQueryStatus: func, onZoom: func, resizeCoords: shape(), grabDataForDownload: func, + colors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: string.isRequired, + }).isRequired + ), } RefreshingGraph.defaultProps = { diff --git a/ui/src/shared/components/ResizeContainer.js b/ui/src/shared/components/ResizeContainer.js index a67a40374b..0857a98acd 100644 --- a/ui/src/shared/components/ResizeContainer.js +++ b/ui/src/shared/components/ResizeContainer.js @@ -34,6 +34,7 @@ class ResizeContainer extends Component { componentDidMount() { this.setState({ bottomHeightPixels: this.bottom.getBoundingClientRect().height, + topHeightPixels: this.top.getBoundingClientRect().height, }) } @@ -92,11 +93,18 @@ class ResizeContainer extends Component { topHeight: `${newTopPanelPercent}%`, bottomHeight: `${newBottomPanelPercent}%`, bottomHeightPixels, + topHeightPixels, }) } render() { - const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state + const { + topHeightPixels, + bottomHeightPixels, + topHeight, + bottomHeight, + isDragging, + } = this.state const {containerClass, children, theme} = this.props if (React.Children.count(children) > maximumNumChildren) { @@ -116,9 +124,14 @@ class ResizeContainer extends Component { onMouseMove={this.handleDrag} ref={r => (this.resizeContainer = r)} > -
+
(this.top = r)} + > {React.cloneElement(children[0], { resizerBottomHeight: bottomHeightPixels, + resizerTopHeight: topHeightPixels, })}
{React.cloneElement(children[1], { resizerBottomHeight: bottomHeightPixels, + resizerTopHeight: topHeightPixels, })}
diff --git a/ui/src/shared/constants/gaugeSpecs.js b/ui/src/shared/constants/gaugeSpecs.js new file mode 100644 index 0000000000..f876667304 --- /dev/null +++ b/ui/src/shared/constants/gaugeSpecs.js @@ -0,0 +1,16 @@ +export const GAUGE_SPECS = { + degree: Math.PI / 180, + lineCount: 6, + lineColor: '#545667', + labelColor: '#8E91A1', + labelFontSize: 13, + lineStrokeSmall: 1, + lineStrokeLarge: 3, + tickSizeSmall: 9, + tickSizeLarge: 18, + minFontSize: 22, + minLineWidth: 24, + valueColor: '#ffffff', + needleColor0: '#434453', + needleColor1: '#ffffff', +} diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index cfb032a7de..b8824ab25d 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -2,7 +2,7 @@ import app from './app' import auth from './auth' import errors from './errors' import links from './links' -import notifications from './notifications' +import {notifications, dismissedNotifications} from './notifications' import sources from './sources' export default { @@ -11,5 +11,6 @@ export default { errors, links, notifications, + dismissedNotifications, sources, } diff --git a/ui/src/shared/reducers/notifications.js b/ui/src/shared/reducers/notifications.js index c4275fcd8a..6767a6f289 100644 --- a/ui/src/shared/reducers/notifications.js +++ b/ui/src/shared/reducers/notifications.js @@ -1,11 +1,7 @@ import u from 'updeep' +import _ from 'lodash' -function getInitialState() { - return {} -} -const initialState = getInitialState() - -const notificationsReducer = (state = initialState, action) => { +export const notifications = (state = {}, action) => { switch (action.type) { case 'NOTIFICATION_RECEIVED': { const {type, message} = action.payload @@ -16,11 +12,38 @@ const notificationsReducer = (state = initialState, action) => { return u(u.omit(type), state) } case 'ALL_NOTIFICATIONS_DISMISSED': { - return getInitialState() + // Reset to initial state + return {} } } return state } -export default notificationsReducer +export const getNotificationID = (message, type) => _.snakeCase(message) + type + +export const dismissedNotifications = (state = {}, action) => { + switch (action.type) { + case 'NOTIFICATION_RECEIVED': { + const {type, message, once} = action.payload + if (once) { + // Create a message ID in a deterministic way, also with its type + const messageID = getNotificationID(message, type) + if (state[messageID]) { + // Message action called with once option but we've already seen it + return state + } + // Message action called with once option and it's not present on + // the persisted state + return { + ...state, + [messageID]: true, + } + } + // Message action not called with once option + return state + } + } + + return state +} diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 31e79b3d9b..5a377d739c 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -30,6 +30,7 @@ @import 'components/ceo-display-options'; @import 'components/confirm-buttons'; @import 'components/code-mirror-theme'; +@import 'components/color-dropdown'; @import 'components/custom-time-range'; @import 'components/dygraphs'; @import 'components/fancy-scrollbars'; diff --git a/ui/src/style/components/ceo-display-options.scss b/ui/src/style/components/ceo-display-options.scss index 44d77270d5..c2c7ea7f03 100644 --- a/ui/src/style/components/ceo-display-options.scss +++ b/ui/src/style/components/ceo-display-options.scss @@ -2,6 +2,9 @@ Cell Editor Overlay - Display Options ------------------------------------------------------ */ + +$graph-type--gutter: 4px; + .display-options { height: 100%; display: flex; @@ -11,16 +14,25 @@ align-items: stretch; } .display-options--cell { - position: relative; - border-radius: 3px; - background-color: $g3-castle; - padding: 30px; flex: 1 0 0; margin-right: 8px; + border-radius: 3px; + background-color: $g3-castle; + + &:last-of-type { + margin: 0; + } } .display-options--cellx2 { flex: 2 0 0; } +.display-options--cell-wrapper { + width: 100%; + position: relative; + display: inline-block; + padding: 30px; +} + .display-options--header { margin: 0 0 12px 0; font-weight: 400; @@ -28,41 +40,42 @@ @include no-user-select(); } .viz-type-selector { - display: flex; - flex-wrap: wrap; - position: absolute; - top: 60px; - left: 26px; - height: calc(100% - 85px); - width: calc(100% - 52px); - margin: 0; + width: 100%; + display: inline-block; + margin: 0 (-$graph-type--gutter / 2); + margin-bottom: -$graph-type--gutter; } .viz-type-selector--option { - flex: 1 0 33.3333%; - height: 50%; - padding: 4px; + float: left; + width: 33.3333%; + padding-bottom: 33.3333%; + position: relative; > div > p { margin: 0; font-size: 14px; font-weight: 900; position: absolute; - bottom: 6.25%; - left: 50%; - transform: translate(-50%, 50%); + bottom: 18px; + left: 10px; + width: calc(100% - 20px); + text-align: center; display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } // Actual "card" > div { - background-color: $g3-castle; - border: 2px solid $g4-onyx; + background-color: $g2-kevlar; color: $g11-sidewalk; - border-radius: 3px; - width: 100%; - height: 100%; - display: block; - position: relative; + border-radius: 4px; + width: calc(100% - #{$graph-type--gutter}); + height: calc(100% - #{$graph-type--gutter}); + position: absolute; + top: $graph-type--gutter / 2; + left: $graph-type--gutter / 2; transition: color 0.25s ease, border-color 0.25s ease, @@ -71,61 +84,93 @@ &:hover { cursor: pointer; background-color: $g4-onyx; - border-color: $g5-pepper; color: $g15-platinum; } } } +// Increase options per row as screen enlarges +@media only screen and (min-width: 1000px) { + .viz-type-selector--option { + width: 25%; + padding-bottom: 25%; + } +} +@media only screen and (min-width: 1270px) { + .viz-type-selector--option { + width: 20%; + padding-bottom: 20%; + } +} +@media only screen and (min-width: 1600px) { + .viz-type-selector--option { + width: 16.6667%; + padding-bottom: 16.6667%; + } +} +@media only screen and (min-width: 2000px) { + .viz-type-selector--option { + width: 12.5%; + padding-bottom: 12.5%; + } +} + // Active state "card" .viz-type-selector--option.active > div, .viz-type-selector--option.active > div:hover { background-color: $g5-pepper; - border-color: $g7-graphite; color: $g18-cloud; } .viz-type-selector--graphic { - width: calc(100% - 48px); - height: 50%; + width: calc(100% - 54px); + height: calc(100% - 54px); position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); - > svg { + > svg, + > svg * { transform: translate3d(0,0,0); + } + > svg { width: 100%; height: 100%; } } .viz-type-selector--graphic-line { - stroke-width: 3px; + stroke-width: 2px; fill: none; stroke-linecap: round; stroke-miterlimit: 10; - transition: all 0.5s ease; + // transition: all 0.5s ease; &.graphic-line-a {stroke: $g11-sidewalk;} &.graphic-line-b {stroke: $g9-mountain;} &.graphic-line-c {stroke: $g7-graphite;} + &.graphic-line-d {stroke: $g13-mist;} } .viz-type-selector--graphic-fill { - opacity: 0.035; - transition: opacity 0.5s ease; + opacity: 0.045; + // transition: opacity 0.5s ease; - &.graphic-fill-a {fill: $g11-sidewalk;} &.graphic-fill-b {fill: $g9-mountain;} + &.graphic-fill-a {fill: $g11-sidewalk;} + &.graphic-fill-b {fill: $g9-mountain;} &.graphic-fill-c {fill: $g7-graphite;} + &.graphic-fill-d {fill: $g13-mist; opacity: 1;} } .viz-type-selector--option.active .viz-type-selector--graphic { .viz-type-selector--graphic-line.graphic-line-a {stroke: $c-pool;} .viz-type-selector--graphic-line.graphic-line-b {stroke: $c-dreamsicle;} .viz-type-selector--graphic-line.graphic-line-c {stroke: $c-rainforest;} + .viz-type-selector--graphic-line.graphic-line-d {stroke: $g17-whisper;} .viz-type-selector--graphic-fill.graphic-fill-a {fill: $c-pool;} .viz-type-selector--graphic-fill.graphic-fill-b {fill: $c-dreamsicle;} .viz-type-selector--graphic-fill.graphic-fill-c {fill: $c-rainforest;} .viz-type-selector--graphic-fill.graphic-fill-a, .viz-type-selector--graphic-fill.graphic-fill-b, - .viz-type-selector--graphic-fill.graphic-fill-c {opacity: 0.18;} + .viz-type-selector--graphic-fill.graphic-fill-c {opacity: 0.22;} + .viz-type-selector--graphic-fill.graphic-fill-d {fill: $g17-whisper; opacity: 1;} } @@ -150,3 +195,52 @@ padding-left: 6px; @include no-user-select(); } + + + +/* + Cell Editor Overlay - Gauge Controls + ------------------------------------------------------ +*/ +.gauge-controls { + width: 100%; +} + +.gauge-controls--section { + width: 100%; + display: flex; + flex-wrap: nowrap; + align-items: center; + height: 30px; + margin-bottom: 8px; +} +button.btn.btn-primary.btn-sm.gauge-controls--add-threshold { + width: 100%; +} + +.gauge-controls--label { + height: 30px; + background-color: $g4-onyx; + font-weight: 600; + color: $g11-sidewalk; + padding: 0 11px; + border-radius: 4px; + line-height: 30px; + @include no-user-select(); + width: 120px; +} +.gauge-controls--label-editable { + height: 30px; + font-weight: 600; + color: $g16-pearl; + padding: 0 11px; + border-radius: 4px; + line-height: 30px; + @include no-user-select(); + width: 90px; +} + +.gauge-controls--input { + flex: 1 0 0; + margin: 0 4px; +} diff --git a/ui/src/style/components/color-dropdown.scss b/ui/src/style/components/color-dropdown.scss new file mode 100644 index 0000000000..ab7dd42bc1 --- /dev/null +++ b/ui/src/style/components/color-dropdown.scss @@ -0,0 +1,98 @@ +/* + Color Dropdown + ------------------------------------------------------------------------------ +*/ + +$color-dropdown--circle: 14px; + +.color-dropdown { + width: 140px; + height: 30px; + position: relative; +} + +.color-dropdown--toggle { + width: 100%; + position: relative; +} +.color-dropdown--toggle span.caret { + font-style: normal !important; + position: absolute; + top: 50%; + right: 11px; + transform: translateY(-50%); +} + +.color-dropdown--menu { + position: absolute; + top: 30px; + left: 0; + z-index: 2; + width: 100%; + border-radius: 4px; + box-shadow: 0 2px 5px 0.6px fade-out($g0-obsidian, 0.7); + @include gradient-h($g0-obsidian,$g2-kevlar); +} +.color-dropdown--item { + @include no-user-select(); + width: 100%; + height: 28px; + position: relative; + color: $g11-sidewalk; + transition: + color 0.25s ease, + background-color 0.25s ease; + + &:hover { + background-color: $g4-onyx; + color: $g18-cloud; + } + &:hover, + &:hover > * { + cursor: pointer !important; + } + &.active { + background-color: $g3-castle; + color: $g15-platinum; + } + &:first-child { + border-radius: 4px 4px 0 0; + } + &:last-child { + border-radius: 0 0 4px 4px; + } +} +.color-dropdown--swatch, +.color-dropdown--name { + position: absolute; + top: 50%; + transform: translateY(-50%); +} +.color-dropdown--swatch { + width: $color-dropdown--circle; + height: $color-dropdown--circle; + border-radius: 50%; + left: 11px; +} +.color-dropdown--name { + text-align: left; + right: 11px; + left: 34px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-weight: 600; + text-transform: capitalize; +} +.color-dropdown .color-dropdown--menu .fancy-scroll--container .fancy-scroll--track-v .fancy-scroll--thumb-v { + @include gradient-v($g9-mountain,$g7-graphite); +} +.color-dropdown--toggle.color-dropdown__disabled { + color: $g7-graphite; + font-style: italic; + cursor: not-allowed; +} +.color-dropdown--toggle.color-dropdown__disabled > .color-dropdown--swatch { + background-color: $g7-graphite !important; +} diff --git a/ui/src/style/components/dygraphs.scss b/ui/src/style/components/dygraphs.scss index b8574eff49..5f64c1ba39 100644 --- a/ui/src/style/components/dygraphs.scss +++ b/ui/src/style/components/dygraphs.scss @@ -83,6 +83,11 @@ &.graph-single-stat { top: 0; } + + > canvas.gauge { + width: 100% !important; + height: 100% !important; + } } .single-stat--value { position: absolute; diff --git a/ui/src/style/components/kapacitor-logs-table.scss b/ui/src/style/components/kapacitor-logs-table.scss index fed167ff9d..ff1eb79853 100644 --- a/ui/src/style/components/kapacitor-logs-table.scss +++ b/ui/src/style/components/kapacitor-logs-table.scss @@ -32,30 +32,13 @@ $logs-margin: 4px; height: calc(100% - #{$logs-table-header-height}) !important; } -.logs-table, -.logs-table--row { - display: flex; - align-items: stretch; - flex-direction: column; -} -@keyframes LogsFadeIn { - from { - background-color: $g6-smoke; - } - to { - background-color: transparent; - } -} .logs-table { - flex-direction: column-reverse; + height: 100%; } .logs-table--row { + height: 87px; // Fixed height, required for Infinite Scroll, allows for 2 tags / fields per line padding: 8px ($logs-table-padding - 16px) 8px ($logs-table-padding / 2); border-bottom: 2px solid $g3-castle; - animation-name: LogsFadeIn; - animation-duration: 2.5s; - animation-iteration-count: 1; - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); transition: background-color 0.25s ease; &:hover { diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss index 8f6e91161c..22bc66f395 100644 --- a/ui/src/style/components/tables.scss +++ b/ui/src/style/components/tables.scss @@ -182,7 +182,8 @@ $table-tab-scrollbar-height: 6px; Alert History "Page" ---------------------------------------------- */ -.alert-history-page { +.alert-history-page, +.hosts-list-page { .page-contents > .container-fluid, .page-contents > .container-fluid > .row, .page-contents > .container-fluid > .row > .col-md-12, @@ -238,7 +239,7 @@ $table-tab-scrollbar-height: 6px; color: $g17-whisper; } .alert-history-table--tbody { - flex: 1 0 0%; + flex: 1 0 0; width: 100%; } .alert-history-table--tr { @@ -276,3 +277,48 @@ table .table-cell-nowrap { overflow: hidden; text-overflow: ellipsis; } + +/* + Hosts "Table" + ---------------------------------------------- +*/ + +.hosts-table { + height: 100%; + display: flex; + flex-direction: column; + align-items: stretch; +} +.hosts-table--thead { + display: flex; + width: 100%; + border-bottom: 2px solid $g5-pepper; +} +.hosts-table--th { + @include no-user-select(); + padding: 8px; + font-size: 13px; + font-weight: 500; + color: $g17-whisper; +} +.hosts-table--tbody { + flex: 1 0 0; + width: 100%; +} +.hosts-table--tr { + display: flex; + width: 100%; + &:hover { + background-color: $g4-onyx; + } +} +.hosts-table--td { + font-size: 12px; + font-family: $code-font; + font-weight: 500; + padding: 4px 8px; + line-height: 1.42857143em; + color: $g13-mist; + white-space: pre-wrap; + word-break: break-all; +} diff --git a/ui/src/style/pages/overlay-technology.scss b/ui/src/style/pages/overlay-technology.scss index 8cc608ac2d..38aff1a1eb 100644 --- a/ui/src/style/pages/overlay-technology.scss +++ b/ui/src/style/pages/overlay-technology.scss @@ -45,7 +45,7 @@ $overlay-z: 100; background-color: $g2-kevlar; } .overlay-controls .nav-tablist { - width: 200px; + width: 230px; li { white-space: nowrap;