Merge branch 'master' into style/table-thresholds-polish

pull/10616/head
Alex P 2018-03-22 14:17:26 -07:00
commit 0a8ac81c02
28 changed files with 729 additions and 671 deletions

View File

@ -265,16 +265,18 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
} }
} }
sortBy := &TableColumn{ sortBy := &RenamableField{
InternalName: c.TableOptions.SortBy.InternalName, InternalName: c.TableOptions.SortBy.InternalName,
DisplayName: c.TableOptions.SortBy.DisplayName, DisplayName: c.TableOptions.SortBy.DisplayName,
Visible: c.TableOptions.SortBy.Visible,
} }
columnNames := make([]*TableColumn, len(c.TableOptions.ColumnNames)) fieldNames := make([]*RenamableField, len(c.TableOptions.FieldNames))
for i, column := range c.TableOptions.ColumnNames { for i, field := range c.TableOptions.FieldNames {
columnNames[i] = &TableColumn{ fieldNames[i] = &RenamableField{
InternalName: column.InternalName, InternalName: field.InternalName,
DisplayName: column.DisplayName, DisplayName: field.DisplayName,
Visible: field.Visible,
} }
} }
@ -283,7 +285,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
VerticalTimeAxis: c.TableOptions.VerticalTimeAxis, VerticalTimeAxis: c.TableOptions.VerticalTimeAxis,
SortBy: sortBy, SortBy: sortBy,
Wrapping: c.TableOptions.Wrapping, Wrapping: c.TableOptions.Wrapping,
ColumnNames: columnNames, FieldNames: fieldNames,
FixFirstColumn: c.TableOptions.FixFirstColumn, FixFirstColumn: c.TableOptions.FixFirstColumn,
} }
@ -431,20 +433,22 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
tableOptions := chronograf.TableOptions{} tableOptions := chronograf.TableOptions{}
if c.TableOptions != nil { if c.TableOptions != nil {
sortBy := chronograf.TableColumn{} sortBy := chronograf.RenamableField{}
if c.TableOptions.SortBy != nil { if c.TableOptions.SortBy != nil {
sortBy.InternalName = c.TableOptions.SortBy.InternalName sortBy.InternalName = c.TableOptions.SortBy.InternalName
sortBy.DisplayName = c.TableOptions.SortBy.DisplayName sortBy.DisplayName = c.TableOptions.SortBy.DisplayName
sortBy.Visible = c.TableOptions.SortBy.Visible
} }
tableOptions.SortBy = sortBy tableOptions.SortBy = sortBy
columnNames := make([]chronograf.TableColumn, len(c.TableOptions.ColumnNames)) fieldNames := make([]chronograf.RenamableField, len(c.TableOptions.FieldNames))
for i, column := range c.TableOptions.ColumnNames { for i, field := range c.TableOptions.FieldNames {
columnNames[i] = chronograf.TableColumn{} fieldNames[i] = chronograf.RenamableField{}
columnNames[i].InternalName = column.InternalName fieldNames[i].InternalName = field.InternalName
columnNames[i].DisplayName = column.DisplayName fieldNames[i].DisplayName = field.DisplayName
fieldNames[i].Visible = field.Visible
} }
tableOptions.ColumnNames = columnNames tableOptions.FieldNames = fieldNames
tableOptions.TimeFormat = c.TableOptions.TimeFormat tableOptions.TimeFormat = c.TableOptions.TimeFormat
tableOptions.VerticalTimeAxis = c.TableOptions.VerticalTimeAxis tableOptions.VerticalTimeAxis = c.TableOptions.VerticalTimeAxis
tableOptions.Wrapping = c.TableOptions.Wrapping tableOptions.Wrapping = c.TableOptions.Wrapping

View File

@ -12,7 +12,7 @@ It has these top-level messages:
Dashboard Dashboard
DashboardCell DashboardCell
TableOptions TableOptions
TableColumn RenamableField
Color Color
Legend Legend
Axis Axis
@ -316,12 +316,12 @@ func (m *DashboardCell) GetTableOptions() *TableOptions {
} }
type TableOptions struct { type TableOptions struct {
TimeFormat string `protobuf:"bytes,1,opt,name=timeFormat,proto3" json:"timeFormat,omitempty"` TimeFormat string `protobuf:"bytes,1,opt,name=timeFormat,proto3" json:"timeFormat,omitempty"`
VerticalTimeAxis bool `protobuf:"varint,2,opt,name=verticalTimeAxis,proto3" json:"verticalTimeAxis,omitempty"` VerticalTimeAxis bool `protobuf:"varint,2,opt,name=verticalTimeAxis,proto3" json:"verticalTimeAxis,omitempty"`
SortBy *TableColumn `protobuf:"bytes,3,opt,name=sortBy" json:"sortBy,omitempty"` SortBy *RenamableField `protobuf:"bytes,3,opt,name=sortBy" json:"sortBy,omitempty"`
Wrapping string `protobuf:"bytes,4,opt,name=wrapping,proto3" json:"wrapping,omitempty"` Wrapping string `protobuf:"bytes,4,opt,name=wrapping,proto3" json:"wrapping,omitempty"`
ColumnNames []*TableColumn `protobuf:"bytes,5,rep,name=columnNames" json:"columnNames,omitempty"` FieldNames []*RenamableField `protobuf:"bytes,5,rep,name=fieldNames" json:"fieldNames,omitempty"`
FixFirstColumn bool `protobuf:"varint,6,opt,name=fixFirstColumn,proto3" json:"fixFirstColumn,omitempty"` FixFirstColumn bool `protobuf:"varint,6,opt,name=fixFirstColumn,proto3" json:"fixFirstColumn,omitempty"`
} }
func (m *TableOptions) Reset() { *m = TableOptions{} } func (m *TableOptions) Reset() { *m = TableOptions{} }
@ -343,7 +343,7 @@ func (m *TableOptions) GetVerticalTimeAxis() bool {
return false return false
} }
func (m *TableOptions) GetSortBy() *TableColumn { func (m *TableOptions) GetSortBy() *RenamableField {
if m != nil { if m != nil {
return m.SortBy return m.SortBy
} }
@ -357,9 +357,9 @@ func (m *TableOptions) GetWrapping() string {
return "" return ""
} }
func (m *TableOptions) GetColumnNames() []*TableColumn { func (m *TableOptions) GetFieldNames() []*RenamableField {
if m != nil { if m != nil {
return m.ColumnNames return m.FieldNames
} }
return nil return nil
} }
@ -371,30 +371,38 @@ func (m *TableOptions) GetFixFirstColumn() bool {
return false return false
} }
type TableColumn struct { type RenamableField struct {
InternalName string `protobuf:"bytes,1,opt,name=internalName,proto3" json:"internalName,omitempty"` InternalName string `protobuf:"bytes,1,opt,name=internalName,proto3" json:"internalName,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=displayName,proto3" json:"displayName,omitempty"` DisplayName string `protobuf:"bytes,2,opt,name=displayName,proto3" json:"displayName,omitempty"`
Visible bool `protobuf:"varint,3,opt,name=visible,proto3" json:"visible,omitempty"`
} }
func (m *TableColumn) Reset() { *m = TableColumn{} } func (m *RenamableField) Reset() { *m = RenamableField{} }
func (m *TableColumn) String() string { return proto.CompactTextString(m) } func (m *RenamableField) String() string { return proto.CompactTextString(m) }
func (*TableColumn) ProtoMessage() {} func (*RenamableField) ProtoMessage() {}
func (*TableColumn) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } func (*RenamableField) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
func (m *TableColumn) GetInternalName() string { func (m *RenamableField) GetInternalName() string {
if m != nil { if m != nil {
return m.InternalName return m.InternalName
} }
return "" return ""
} }
func (m *TableColumn) GetDisplayName() string { func (m *RenamableField) GetDisplayName() string {
if m != nil { if m != nil {
return m.DisplayName return m.DisplayName
} }
return "" return ""
} }
func (m *RenamableField) GetVisible() bool {
if m != nil {
return m.Visible
}
return false
}
type Color struct { type Color struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"`
@ -1296,7 +1304,7 @@ func init() {
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard") proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell") proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
proto.RegisterType((*TableOptions)(nil), "internal.TableOptions") proto.RegisterType((*TableOptions)(nil), "internal.TableOptions")
proto.RegisterType((*TableColumn)(nil), "internal.TableColumn") proto.RegisterType((*RenamableField)(nil), "internal.RenamableField")
proto.RegisterType((*Color)(nil), "internal.Color") proto.RegisterType((*Color)(nil), "internal.Color")
proto.RegisterType((*Legend)(nil), "internal.Legend") proto.RegisterType((*Legend)(nil), "internal.Legend")
proto.RegisterType((*Axis)(nil), "internal.Axis") proto.RegisterType((*Axis)(nil), "internal.Axis")
@ -1322,104 +1330,105 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{ var fileDescriptorInternal = []byte{
// 1573 bytes of a gzipped FileDescriptorProto // 1586 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
0x10, 0x97, 0x93, 0x38, 0x89, 0x27, 0xd7, 0xe3, 0x64, 0x8e, 0xd6, 0x14, 0x09, 0x05, 0x8b, 0x3f, 0x10, 0x97, 0x93, 0x38, 0x89, 0x27, 0xd7, 0xe3, 0x64, 0x4e, 0xad, 0x29, 0x12, 0x0a, 0x16, 0x7f,
0xe1, 0x4f, 0x0f, 0x94, 0x0a, 0x81, 0x2a, 0xa8, 0x94, 0xbb, 0xd0, 0x72, 0xf4, 0xda, 0xbb, 0x6e, 0xc2, 0x9f, 0x1e, 0x55, 0x2a, 0xa4, 0xaa, 0x82, 0x4a, 0xb9, 0x0b, 0x2d, 0x47, 0xaf, 0xbd, 0xeb,
0xee, 0x8e, 0x27, 0x54, 0x6d, 0x92, 0x4d, 0x62, 0xd5, 0xb1, 0xcd, 0x7a, 0x7d, 0x17, 0xf3, 0x01, 0xe6, 0xee, 0x78, 0x42, 0xd5, 0x26, 0x99, 0x24, 0x56, 0x1d, 0xdb, 0xac, 0xed, 0xbb, 0x98, 0x8f,
0xf8, 0x18, 0x48, 0x48, 0xf0, 0x05, 0x10, 0x2f, 0x3c, 0xf1, 0xce, 0x07, 0xe1, 0x2b, 0xc0, 0x23, 0xc0, 0x87, 0x40, 0x42, 0x82, 0x2f, 0x80, 0x78, 0xe1, 0x89, 0x77, 0x3e, 0x08, 0x5f, 0x01, 0x1e,
0x9a, 0xdd, 0xb5, 0xb3, 0xb9, 0xa4, 0x55, 0x91, 0x10, 0x6f, 0xfb, 0x9b, 0x19, 0xcf, 0xce, 0xce, 0xd1, 0xec, 0xae, 0x1d, 0xe7, 0x92, 0x56, 0x45, 0x42, 0xbc, 0xed, 0x6f, 0x66, 0x3c, 0xbb, 0xf3,
0xcc, 0x6f, 0x76, 0x0d, 0xdb, 0x41, 0x24, 0x18, 0x8f, 0x68, 0xb8, 0x97, 0xf0, 0x58, 0xc4, 0x6e, 0x7f, 0x0c, 0xdb, 0x5e, 0x90, 0xa0, 0x08, 0xb8, 0xbf, 0x17, 0x89, 0x30, 0x09, 0xed, 0x66, 0x8e,
0xb3, 0xc0, 0xfe, 0x9f, 0x15, 0xa8, 0x0f, 0xe2, 0x8c, 0x8f, 0x98, 0xbb, 0x0d, 0x95, 0xc3, 0xbe, 0xdd, 0x3f, 0x2b, 0x50, 0x1f, 0x84, 0xa9, 0x18, 0xa1, 0xbd, 0x0d, 0x95, 0xc3, 0xbe, 0x63, 0xb4,
0x67, 0xb5, 0xad, 0x4e, 0x95, 0x54, 0x0e, 0xfb, 0xae, 0x0b, 0xb5, 0x47, 0x74, 0xce, 0xbc, 0x4a, 0x8d, 0x4e, 0x95, 0x55, 0x0e, 0xfb, 0xb6, 0x0d, 0xb5, 0x27, 0x7c, 0x8e, 0x4e, 0xa5, 0x6d, 0x74,
0xdb, 0xea, 0x38, 0x44, 0xae, 0x51, 0x76, 0x9a, 0x27, 0xcc, 0xab, 0x2a, 0x19, 0xae, 0xdd, 0x9b, 0x2c, 0x26, 0xcf, 0x44, 0x3b, 0xcd, 0x22, 0x74, 0xaa, 0x8a, 0x46, 0x67, 0xfb, 0x26, 0x34, 0xcf,
0xd0, 0x3c, 0x4b, 0xd1, 0xdb, 0x9c, 0x79, 0x35, 0x29, 0x2f, 0x31, 0xea, 0x4e, 0x68, 0x9a, 0x5e, 0x62, 0xd2, 0x36, 0x47, 0xa7, 0x26, 0xe9, 0x05, 0x26, 0xde, 0x09, 0x8f, 0xe3, 0xcb, 0x50, 0x8c,
0xc6, 0x7c, 0xec, 0xd9, 0x4a, 0x57, 0x60, 0x77, 0x07, 0xaa, 0x67, 0xe4, 0xc8, 0xab, 0x4b, 0x31, 0x1d, 0x53, 0xf1, 0x72, 0x6c, 0xef, 0x40, 0xf5, 0x8c, 0x1d, 0x39, 0x75, 0x49, 0xa6, 0xa3, 0xed,
0x2e, 0x5d, 0x0f, 0x1a, 0x7d, 0x36, 0xa1, 0x59, 0x28, 0xbc, 0x46, 0xdb, 0xea, 0x34, 0x49, 0x01, 0x40, 0xa3, 0x8f, 0x13, 0x9e, 0xfa, 0x89, 0xd3, 0x68, 0x1b, 0x9d, 0x26, 0xcb, 0x21, 0xe9, 0x39,
0xd1, 0xcf, 0x29, 0x0b, 0xd9, 0x94, 0xd3, 0x89, 0xd7, 0x54, 0x7e, 0x0a, 0xec, 0xee, 0x81, 0x7b, 0x45, 0x1f, 0xa7, 0x82, 0x4f, 0x9c, 0xa6, 0xd2, 0x93, 0x63, 0x7b, 0x0f, 0xec, 0xc3, 0x20, 0xc6,
0x18, 0xa5, 0x6c, 0x94, 0x71, 0x36, 0x78, 0x1a, 0x24, 0xe7, 0x8c, 0x07, 0x93, 0xdc, 0x73, 0xa4, 0x51, 0x2a, 0x70, 0xf0, 0xdc, 0x8b, 0xce, 0x51, 0x78, 0x93, 0xcc, 0xb1, 0xa4, 0x82, 0x0d, 0x1c,
0x83, 0x0d, 0x1a, 0xdc, 0xe5, 0x21, 0x13, 0x14, 0xf7, 0x06, 0xe9, 0xaa, 0x80, 0xae, 0x0f, 0x5b, 0xba, 0xe5, 0x31, 0x26, 0x9c, 0xee, 0x06, 0xa9, 0x2a, 0x87, 0xb6, 0x0b, 0x5b, 0x83, 0x19, 0x17,
0x83, 0x19, 0xe5, 0x6c, 0x3c, 0x60, 0x23, 0xce, 0x84, 0xd7, 0x92, 0xea, 0x15, 0x19, 0xda, 0x1c, 0x38, 0x1e, 0xe0, 0x48, 0x60, 0xe2, 0xb4, 0x24, 0x7b, 0x85, 0x46, 0x32, 0xc7, 0x62, 0xca, 0x03,
0xf3, 0x29, 0x8d, 0x82, 0xef, 0xa8, 0x08, 0xe2, 0xc8, 0xdb, 0x52, 0x36, 0xa6, 0x0c, 0xb3, 0x44, 0xef, 0x3b, 0x9e, 0x78, 0x61, 0xe0, 0x6c, 0x29, 0x99, 0x32, 0x8d, 0xbc, 0xc4, 0x42, 0x1f, 0x9d,
0xe2, 0x90, 0x79, 0xd7, 0x54, 0x96, 0x70, 0xed, 0xff, 0x6a, 0x81, 0xd3, 0xa7, 0xe9, 0x6c, 0x18, 0x6b, 0xca, 0x4b, 0x74, 0x76, 0x7f, 0x35, 0xc0, 0xea, 0xf3, 0x78, 0x36, 0x0c, 0xb9, 0x18, 0xbf,
0x53, 0x3e, 0x7e, 0xa1, 0x5c, 0xdf, 0x02, 0x7b, 0xc4, 0xc2, 0x30, 0xf5, 0xaa, 0xed, 0x6a, 0xa7, 0x92, 0xaf, 0x6f, 0x81, 0x39, 0x42, 0xdf, 0x8f, 0x9d, 0x6a, 0xbb, 0xda, 0x69, 0x75, 0x6f, 0xec,
0xd5, 0xbd, 0xb1, 0x57, 0x16, 0xb1, 0xf4, 0x73, 0xc0, 0xc2, 0x90, 0x28, 0x2b, 0xf7, 0x23, 0x70, 0x15, 0x41, 0x2c, 0xf4, 0x1c, 0xa0, 0xef, 0x33, 0x25, 0x65, 0xdf, 0x06, 0x2b, 0xc1, 0x79, 0xe4,
0x04, 0x9b, 0x27, 0x21, 0x15, 0x2c, 0xf5, 0x6a, 0xf2, 0x13, 0x77, 0xf9, 0xc9, 0xa9, 0x56, 0x91, 0xf3, 0x04, 0x63, 0xa7, 0x26, 0x3f, 0xb1, 0x97, 0x9f, 0x9c, 0x6a, 0x16, 0x5b, 0x0a, 0xad, 0x99,
0xa5, 0xd1, 0xda, 0x51, 0xec, 0xf5, 0xa3, 0xf8, 0xbf, 0x55, 0xe1, 0xda, 0xca, 0x76, 0xee, 0x16, 0x62, 0xae, 0x9b, 0xe2, 0xfe, 0x56, 0x85, 0x6b, 0x2b, 0xd7, 0xd9, 0x5b, 0x60, 0x2c, 0xe4, 0xcb,
0x58, 0x0b, 0x19, 0xb9, 0x4d, 0xac, 0x05, 0xa2, 0x5c, 0x46, 0x6d, 0x13, 0x2b, 0x47, 0x74, 0x29, 0x4d, 0x66, 0x2c, 0x08, 0x65, 0xf2, 0xd5, 0x26, 0x33, 0x32, 0x42, 0x97, 0x32, 0x37, 0x4c, 0x66,
0x7b, 0xc3, 0x26, 0xd6, 0x25, 0xa2, 0x99, 0xec, 0x08, 0x9b, 0x58, 0x33, 0xf7, 0x5d, 0x68, 0x7c, 0x5c, 0x12, 0x9a, 0xc9, 0x8c, 0x30, 0x99, 0x31, 0xb3, 0x3f, 0x80, 0xc6, 0xb7, 0x29, 0x0a, 0x0f,
0x9b, 0x31, 0x1e, 0xb0, 0xd4, 0xb3, 0x65, 0x74, 0x2f, 0x2d, 0xa3, 0x7b, 0x9c, 0x31, 0x9e, 0x93, 0x63, 0xc7, 0x94, 0xaf, 0x7b, 0x6d, 0xf9, 0xba, 0xa7, 0x29, 0x8a, 0x8c, 0xe5, 0x7c, 0xf2, 0x86,
0x42, 0x8f, 0xd9, 0x90, 0xdd, 0xa4, 0x5a, 0x43, 0xae, 0x51, 0x26, 0xb0, 0xf3, 0x1a, 0x4a, 0x86, 0xcc, 0x26, 0x95, 0x1a, 0xf2, 0x4c, 0xb4, 0x84, 0x32, 0xaf, 0xa1, 0x68, 0x74, 0xd6, 0x5e, 0x54,
0x6b, 0x9d, 0x45, 0xd5, 0x0f, 0x98, 0xc5, 0x8f, 0xa1, 0x46, 0x17, 0x2c, 0xf5, 0x1c, 0xe9, 0xff, 0xf9, 0x40, 0x5e, 0xfc, 0x14, 0x6a, 0x7c, 0x81, 0xb1, 0x63, 0x49, 0xfd, 0x6f, 0xbf, 0xc0, 0x61,
0x8d, 0x67, 0x24, 0x6c, 0xaf, 0xb7, 0x60, 0xe9, 0x17, 0x91, 0xe0, 0x39, 0x91, 0xe6, 0xee, 0x3b, 0x7b, 0xbd, 0x05, 0xc6, 0x5f, 0x04, 0x89, 0xc8, 0x98, 0x14, 0xb7, 0xdf, 0x87, 0xfa, 0x28, 0xf4,
0x50, 0x1f, 0xc5, 0x61, 0xcc, 0x53, 0x0f, 0xae, 0x06, 0x76, 0x80, 0x72, 0xa2, 0xd5, 0x6e, 0x07, 0x43, 0x11, 0x3b, 0x70, 0xf5, 0x61, 0x07, 0x44, 0x67, 0x9a, 0x6d, 0x77, 0xa0, 0xee, 0xe3, 0x14,
0xea, 0x21, 0x9b, 0xb2, 0x68, 0x2c, 0x3b, 0xa3, 0xd5, 0xdd, 0x59, 0x1a, 0x1e, 0x49, 0x39, 0xd1, 0x83, 0xb1, 0xcc, 0x8c, 0x56, 0x77, 0x67, 0x29, 0x78, 0x24, 0xe9, 0x4c, 0xf3, 0xed, 0x7b, 0xb0,
0x7a, 0xf7, 0x0e, 0x6c, 0x09, 0x3a, 0x0c, 0xd9, 0x71, 0x82, 0x59, 0x4c, 0x65, 0x97, 0xb4, 0xba, 0x95, 0xf0, 0xa1, 0x8f, 0xc7, 0x11, 0x79, 0x31, 0x96, 0x59, 0xd2, 0xea, 0x5e, 0x2f, 0xc5, 0xa3,
0xd7, 0x8d, 0x7a, 0x18, 0x5a, 0xb2, 0x62, 0x7b, 0xf3, 0x3e, 0x38, 0x65, 0x84, 0x48, 0x92, 0xa7, 0xc4, 0x65, 0x2b, 0xb2, 0x37, 0x1f, 0x82, 0x55, 0xbc, 0x90, 0x8a, 0xe4, 0x39, 0x66, 0xd2, 0xdf,
0x2c, 0x97, 0xf9, 0x76, 0x08, 0x2e, 0xdd, 0x37, 0xc1, 0xbe, 0xa0, 0x61, 0xa6, 0x7a, 0xa5, 0xd5, 0x16, 0xa3, 0xa3, 0xfd, 0x0e, 0x98, 0x17, 0xdc, 0x4f, 0x55, 0xae, 0xb4, 0xba, 0xdb, 0x4b, 0x9d,
0xdd, 0x5e, 0xfa, 0xec, 0x2d, 0x82, 0x94, 0x28, 0xe5, 0x9d, 0xca, 0xa7, 0x96, 0xff, 0x7d, 0x05, 0xbd, 0x85, 0x17, 0x33, 0xc5, 0xbc, 0x57, 0xb9, 0x6b, 0xb8, 0xdf, 0x57, 0x60, 0xab, 0x7c, 0x8f,
0xb6, 0xcc, 0x7d, 0xdc, 0xd7, 0x01, 0x44, 0x30, 0x67, 0xf7, 0x62, 0x3e, 0xa7, 0x42, 0xfb, 0x34, 0xfd, 0x16, 0x40, 0xe2, 0xcd, 0xf1, 0x41, 0x28, 0xe6, 0x3c, 0xd1, 0x3a, 0x4b, 0x14, 0xfb, 0x43,
0x24, 0xee, 0x7b, 0xb0, 0x73, 0xc1, 0xb8, 0x08, 0x46, 0x34, 0x3c, 0x0d, 0xe6, 0x0c, 0xfd, 0xc9, 0xd8, 0xb9, 0x40, 0x91, 0x78, 0x23, 0xee, 0x9f, 0x7a, 0x73, 0x24, 0x7d, 0xf2, 0x96, 0x26, 0x5b,
0x5d, 0x9a, 0x64, 0x4d, 0xee, 0xde, 0x82, 0x7a, 0x1a, 0x73, 0xb1, 0x9f, 0xcb, 0x7a, 0xb7, 0xba, 0xa3, 0xdb, 0xb7, 0xa1, 0x1e, 0x87, 0x22, 0xd9, 0xcf, 0x64, 0xbc, 0x5b, 0x5d, 0x67, 0xf9, 0x0e,
0xaf, 0x5c, 0x39, 0xdb, 0x41, 0x1c, 0x66, 0xf3, 0x88, 0x68, 0x23, 0x24, 0xf0, 0x25, 0xa7, 0x49, 0x86, 0x01, 0x9f, 0xd3, 0xbd, 0x0f, 0x3c, 0xf4, 0xc7, 0x4c, 0xcb, 0x51, 0x0d, 0x5f, 0x0a, 0x1e,
0x12, 0x44, 0xd3, 0x62, 0x48, 0x14, 0xd8, 0xfd, 0x04, 0x5a, 0x23, 0x69, 0x8d, 0x6d, 0x5f, 0x74, 0x45, 0x5e, 0x30, 0xcd, 0xfb, 0x44, 0x8e, 0xed, 0xbb, 0x00, 0x13, 0x12, 0xa6, 0xc4, 0xcf, 0xf3,
0xc7, 0x33, 0xfc, 0x99, 0x96, 0xee, 0xdb, 0xb0, 0x3d, 0x09, 0x16, 0xf7, 0x02, 0x9e, 0x0a, 0xa5, 0xe3, 0xc5, 0x1a, 0x4b, 0xb2, 0xf6, 0x7b, 0xb0, 0x3d, 0xf1, 0x16, 0x0f, 0x3c, 0x11, 0x27, 0x07,
0x96, 0x1d, 0xd3, 0x24, 0x57, 0xa4, 0xfe, 0x00, 0x5a, 0x86, 0x0f, 0xec, 0xfb, 0xc2, 0xb7, 0x24, 0xa1, 0x9f, 0xce, 0x03, 0x99, 0x35, 0x4d, 0x76, 0x85, 0xea, 0x46, 0xb0, 0xbd, 0xaa, 0x85, 0xd2,
0x9d, 0x4a, 0xc4, 0x8a, 0xcc, 0x6d, 0x43, 0x6b, 0x1c, 0xa4, 0x49, 0x48, 0x73, 0x83, 0x97, 0xa6, 0x3f, 0xbf, 0x40, 0xd6, 0x9e, 0xf2, 0xc7, 0x0a, 0xcd, 0x6e, 0x43, 0x6b, 0xec, 0xc5, 0x91, 0xcf,
0xc8, 0x9f, 0x82, 0x2d, 0xbb, 0xc3, 0xe0, 0xb2, 0x53, 0x70, 0x59, 0xce, 0xc8, 0x8a, 0x31, 0x23, 0xb3, 0x52, 0x79, 0x96, 0x49, 0xd4, 0x4d, 0x2e, 0xbc, 0xd8, 0x1b, 0xfa, 0xaa, 0x29, 0x36, 0x59,
0x77, 0xa0, 0xfa, 0x25, 0x5b, 0xe8, 0xb1, 0x89, 0xcb, 0x92, 0xf1, 0x35, 0x83, 0xf1, 0xbb, 0x60, 0x0e, 0xdd, 0x29, 0x98, 0x32, 0x7d, 0x4a, 0xc5, 0x6e, 0xe5, 0xc5, 0x2e, 0x9b, 0x68, 0xa5, 0xd4,
0x9f, 0xcb, 0xd2, 0x2a, 0x26, 0x2a, 0xe0, 0xdf, 0x85, 0xba, 0xea, 0xae, 0xd2, 0xb3, 0x65, 0x78, 0x44, 0x77, 0xa0, 0xfa, 0x25, 0x2e, 0x74, 0x5f, 0xa5, 0x63, 0xd1, 0x12, 0x6a, 0xa5, 0x96, 0xb0,
0x6e, 0x43, 0xeb, 0x98, 0x07, 0x2c, 0x12, 0x8a, 0xc3, 0x3a, 0x50, 0x43, 0xe4, 0xff, 0x62, 0x41, 0x0b, 0xe6, 0xb9, 0x8c, 0xbd, 0x2a, 0x55, 0x05, 0xdc, 0xfb, 0x50, 0x57, 0xe9, 0x57, 0x68, 0x36,
0x4d, 0x96, 0xcc, 0x87, 0xad, 0x90, 0x4d, 0xe9, 0x28, 0xdf, 0x8f, 0xb3, 0x68, 0x9c, 0x7a, 0x56, 0x4a, 0x9a, 0xdb, 0xd0, 0x3a, 0x16, 0x1e, 0x06, 0x89, 0x2a, 0x72, 0x6d, 0x42, 0x89, 0xe4, 0xfe,
0xbb, 0xda, 0xa9, 0x92, 0x15, 0x99, 0x7b, 0x1d, 0xea, 0x43, 0xa5, 0xad, 0xb4, 0xab, 0x1d, 0x87, 0x62, 0x40, 0x4d, 0xc6, 0xd4, 0x85, 0x2d, 0x1f, 0xa7, 0x7c, 0x94, 0xed, 0x87, 0x69, 0x30, 0x8e,
0x68, 0x84, 0xa1, 0x85, 0x74, 0xc8, 0x42, 0x7d, 0x04, 0x05, 0xd0, 0x3a, 0xe1, 0x6c, 0x12, 0x2c, 0x1d, 0xa3, 0x5d, 0xed, 0x54, 0xd9, 0x0a, 0xcd, 0xbe, 0x0e, 0xf5, 0xa1, 0xe2, 0x56, 0xda, 0xd5,
0xf4, 0x31, 0x34, 0x42, 0x79, 0x9a, 0x4d, 0x50, 0xae, 0x4e, 0xa2, 0x11, 0x1e, 0x60, 0x48, 0xd3, 0x8e, 0xc5, 0x34, 0xa2, 0xa7, 0xf9, 0x7c, 0x88, 0xbe, 0x36, 0x41, 0x01, 0x92, 0x8e, 0x04, 0x4e,
0x92, 0xd8, 0xb8, 0x46, 0xcf, 0xe9, 0x88, 0x86, 0x05, 0xb3, 0x15, 0xf0, 0x7f, 0xb7, 0x70, 0xe2, 0xbc, 0x85, 0x36, 0x43, 0x23, 0xa2, 0xc7, 0xe9, 0x84, 0xe8, 0xca, 0x12, 0x8d, 0xc8, 0x80, 0x21,
0xab, 0x49, 0xb5, 0x96, 0xe1, 0x57, 0xa1, 0x89, 0x53, 0xec, 0xc9, 0x05, 0xe5, 0xfa, 0xc0, 0x0d, 0x8f, 0x8b, 0xca, 0xa7, 0x33, 0x69, 0x8e, 0x47, 0xdc, 0xcf, 0x4b, 0x5f, 0x01, 0xf7, 0x77, 0x83,
0xc4, 0xe7, 0x94, 0xbb, 0x1f, 0x42, 0x5d, 0x12, 0x60, 0xc3, 0xd4, 0x2c, 0xdc, 0xc9, 0xac, 0x12, 0x46, 0x82, 0x6a, 0x65, 0x6b, 0x1e, 0x7e, 0x03, 0x9a, 0xd4, 0xe6, 0x9e, 0x5d, 0x70, 0xa1, 0x0d,
0x6d, 0x56, 0xce, 0x95, 0x9a, 0x31, 0x57, 0xca, 0xc3, 0xda, 0xe6, 0x61, 0x6f, 0x81, 0x8d, 0x03, 0x6e, 0x10, 0x3e, 0xe7, 0xc2, 0xfe, 0x04, 0xea, 0xb2, 0x42, 0x36, 0xb4, 0xd5, 0x5c, 0x9d, 0xf4,
0x2a, 0x97, 0xd1, 0x6f, 0xf4, 0xac, 0xc6, 0x98, 0xb2, 0xf2, 0xcf, 0xe0, 0xda, 0xca, 0x8e, 0xe5, 0x2a, 0xd3, 0x62, 0x45, 0xe3, 0xa9, 0x95, 0x1a, 0x4f, 0x61, 0xac, 0x59, 0x36, 0xf6, 0x16, 0x98,
0x4e, 0xd6, 0xea, 0x4e, 0x4b, 0x32, 0x3b, 0x9a, 0xbc, 0x48, 0x96, 0x94, 0x85, 0x6c, 0x24, 0xd8, 0xd4, 0xc1, 0x32, 0xf9, 0xfa, 0x8d, 0x9a, 0x55, 0x9f, 0x53, 0x52, 0xee, 0x19, 0x5c, 0x5b, 0xb9,
0x58, 0xe6, 0xbb, 0x49, 0x4a, 0xec, 0xff, 0x68, 0x2d, 0xfd, 0xca, 0xfd, 0xf0, 0x3e, 0x1b, 0xc5, 0xb1, 0xb8, 0xc9, 0x58, 0xbd, 0x69, 0x59, 0xed, 0x96, 0xae, 0x6e, 0x2a, 0xa5, 0x18, 0x7d, 0x1c,
0xf3, 0x39, 0x8d, 0xc6, 0xda, 0x75, 0x01, 0x31, 0x6f, 0xe3, 0xa1, 0x76, 0x5d, 0x19, 0x0f, 0x11, 0x25, 0x38, 0xd6, 0x59, 0x57, 0x60, 0xf7, 0x47, 0x63, 0xa9, 0x57, 0xde, 0x47, 0x29, 0x3a, 0x0a,
0xf3, 0x44, 0x57, 0xb0, 0xc2, 0x13, 0xec, 0x9d, 0x39, 0xa3, 0x69, 0xc6, 0xd9, 0x9c, 0x45, 0x42, 0xe7, 0x73, 0x1e, 0x8c, 0xb5, 0xea, 0x1c, 0x92, 0xdf, 0xc6, 0x43, 0xad, 0xba, 0x32, 0x1e, 0x12,
0xa7, 0xc0, 0x14, 0xb9, 0x37, 0xa0, 0x21, 0xe8, 0xf4, 0x09, 0x8e, 0x20, 0x5d, 0x49, 0x41, 0xa7, 0x16, 0x91, 0x8e, 0x60, 0x45, 0x44, 0x94, 0x3b, 0x73, 0xe4, 0x71, 0x2a, 0x70, 0x8e, 0x41, 0xa2,
0x0f, 0x58, 0xee, 0xbe, 0x06, 0xce, 0x24, 0x60, 0xe1, 0x58, 0xaa, 0x54, 0x39, 0x9b, 0x52, 0xf0, 0x5d, 0x50, 0x26, 0xd9, 0x37, 0xa0, 0x91, 0xf0, 0xe9, 0x33, 0xea, 0x51, 0x3a, 0x92, 0x09, 0x9f,
0x80, 0xe5, 0xfe, 0xdf, 0x16, 0xd4, 0x07, 0x8c, 0x5f, 0x30, 0xfe, 0x42, 0x17, 0x9d, 0xf9, 0x80, 0x3e, 0xc2, 0xcc, 0x7e, 0x13, 0x2c, 0x59, 0xa5, 0x92, 0xa5, 0xc2, 0xd9, 0x94, 0x84, 0x47, 0x98,
0xa8, 0x3e, 0xe7, 0x01, 0x51, 0xdb, 0xfc, 0x80, 0xb0, 0x97, 0x0f, 0x88, 0x5d, 0xb0, 0x07, 0x7c, 0xb9, 0x7f, 0x1b, 0x50, 0x1f, 0xa0, 0xb8, 0x40, 0xf1, 0x4a, 0x93, 0xb0, 0xbc, 0x61, 0x54, 0x5f,
0x74, 0xd8, 0x97, 0x11, 0x55, 0x89, 0x02, 0xd8, 0x8d, 0xbd, 0x91, 0x08, 0x2e, 0x98, 0x7e, 0x55, 0xb2, 0x61, 0xd4, 0x36, 0x6f, 0x18, 0xe6, 0x72, 0xc3, 0xd8, 0x05, 0x73, 0x20, 0x46, 0x87, 0x7d,
0x68, 0xb4, 0x76, 0xff, 0x35, 0x37, 0x5c, 0xe5, 0xff, 0xf2, 0x71, 0xe1, 0xff, 0x60, 0x41, 0xfd, 0xf9, 0xa2, 0x2a, 0x53, 0x80, 0xb2, 0xb1, 0x37, 0x4a, 0xbc, 0x0b, 0xd4, 0x6b, 0x87, 0x46, 0x6b,
0x88, 0xe6, 0x71, 0x26, 0xd6, 0xba, 0xb6, 0x0d, 0xad, 0x5e, 0x92, 0x84, 0xc1, 0x68, 0x85, 0xa9, 0x03, 0xb2, 0xb9, 0x61, 0xd6, 0xff, 0xcb, 0xed, 0xc3, 0xfd, 0xc1, 0x80, 0xfa, 0x11, 0xcf, 0xc2,
0x86, 0x08, 0x2d, 0x1e, 0x1a, 0xf5, 0x50, 0xb9, 0x30, 0x45, 0x38, 0xfc, 0x0f, 0xe4, 0x9b, 0x40, 0x34, 0x59, 0xcb, 0xda, 0x36, 0xb4, 0x7a, 0x51, 0xe4, 0x7b, 0xa3, 0x95, 0x4a, 0x2d, 0x91, 0x48,
0x5d, 0xf0, 0xc6, 0xf0, 0x57, 0x4f, 0x01, 0xa9, 0xc4, 0xa4, 0xf5, 0x32, 0x11, 0x4f, 0xc2, 0xf8, 0xe2, 0x71, 0x29, 0x1e, 0xca, 0x17, 0x65, 0x12, 0x4d, 0x87, 0x03, 0xb9, 0x34, 0xa8, 0x0d, 0xa0,
0x52, 0x66, 0xa7, 0x49, 0x4a, 0xec, 0xff, 0x51, 0x81, 0xda, 0xff, 0x75, 0x8f, 0x6f, 0x81, 0x15, 0x34, 0x1d, 0xd4, 0xae, 0x20, 0x99, 0xe4, 0xb4, 0x5e, 0x9a, 0x84, 0x13, 0x3f, 0xbc, 0x94, 0xde,
0xe8, 0xe6, 0xb0, 0x82, 0xf2, 0x56, 0x6f, 0x18, 0xb7, 0xba, 0x07, 0x8d, 0x9c, 0xd3, 0x68, 0xca, 0x69, 0xb2, 0x02, 0xbb, 0x7f, 0x54, 0xa0, 0xf6, 0x7f, 0x0d, 0xfa, 0x2d, 0x30, 0x3c, 0x9d, 0x1c,
0x52, 0xaf, 0x29, 0xa7, 0x51, 0x01, 0xa5, 0x46, 0xf2, 0x4e, 0x5d, 0xe7, 0x0e, 0x29, 0x60, 0xc9, 0x86, 0x57, 0x8c, 0xfd, 0x46, 0x69, 0xec, 0x3b, 0xd0, 0xc8, 0x04, 0x0f, 0xa6, 0x18, 0x3b, 0x4d,
0x23, 0x30, 0x78, 0xf4, 0x81, 0xbe, 0xf9, 0x5b, 0x32, 0x22, 0x6f, 0x35, 0x2d, 0x57, 0x2f, 0xfc, 0xd9, 0x8d, 0x72, 0x28, 0x39, 0xb2, 0xee, 0xd4, 0xbc, 0xb7, 0x58, 0x0e, 0x8b, 0x3a, 0x82, 0x52,
0xff, 0xee, 0x86, 0xfd, 0xcb, 0x02, 0xbb, 0x24, 0xe1, 0xc1, 0x2a, 0x09, 0x0f, 0x96, 0x24, 0xec, 0x1d, 0x7d, 0xac, 0x57, 0x83, 0xd6, 0xd5, 0xd1, 0xb2, 0x69, 0x23, 0xf8, 0xef, 0x46, 0xf0, 0x5f,
0xef, 0x17, 0x24, 0xec, 0xef, 0x23, 0x26, 0x27, 0x05, 0x09, 0xc9, 0x09, 0x16, 0xeb, 0x3e, 0x8f, 0x06, 0x98, 0x45, 0x11, 0x1e, 0xac, 0x16, 0xe1, 0xc1, 0xb2, 0x08, 0xfb, 0xfb, 0x79, 0x11, 0xf6,
0xb3, 0x64, 0x3f, 0x57, 0x55, 0x75, 0x48, 0x89, 0xb1, 0x73, 0xbf, 0x9e, 0x31, 0xae, 0x53, 0xed, 0xf7, 0x09, 0xb3, 0x93, 0xbc, 0x08, 0xd9, 0x09, 0x05, 0xeb, 0xa1, 0x08, 0xd3, 0x68, 0x3f, 0x53,
0x10, 0x8d, 0xb0, 0xcf, 0x8f, 0xe4, 0x80, 0x52, 0xc9, 0x55, 0xc0, 0x7d, 0x0b, 0x6c, 0x82, 0xc9, 0x51, 0xb5, 0x58, 0x81, 0x29, 0x73, 0xbf, 0x9e, 0xa1, 0xd0, 0xae, 0xb6, 0x98, 0x46, 0x94, 0xe7,
0x93, 0x19, 0x5e, 0xa9, 0x8b, 0x14, 0x13, 0xa5, 0x45, 0xa7, 0xea, 0xc5, 0xaf, 0x1b, 0xbe, 0x78, 0x47, 0xb2, 0x41, 0x29, 0xe7, 0x2a, 0x60, 0xbf, 0x0b, 0x26, 0x23, 0xe7, 0x49, 0x0f, 0xaf, 0xc4,
0xff, 0xbf, 0x0f, 0xf5, 0xc1, 0x2c, 0x98, 0x88, 0xe2, 0xfd, 0xf4, 0xb2, 0x31, 0xe0, 0x82, 0x39, 0x45, 0x92, 0x99, 0xe2, 0x92, 0x52, 0xf5, 0x4b, 0xa0, 0x13, 0x3e, 0xff, 0x41, 0xf8, 0x08, 0xea,
0x93, 0x3a, 0xa2, 0x4d, 0xfc, 0xc7, 0xe0, 0x94, 0xc2, 0x65, 0x38, 0x96, 0x19, 0x8e, 0x0b, 0xb5, 0x83, 0x99, 0x37, 0x49, 0xf2, 0x05, 0xeb, 0xf5, 0x52, 0x83, 0xf3, 0xe6, 0x28, 0x79, 0x4c, 0x8b,
0xb3, 0x28, 0x10, 0x05, 0xd5, 0x71, 0x8d, 0x87, 0x7d, 0x9c, 0xd1, 0x48, 0x04, 0x22, 0x2f, 0xa8, 0xb8, 0x4f, 0xc1, 0x2a, 0x88, 0xcb, 0xe7, 0x18, 0xe5, 0xe7, 0xd8, 0x50, 0x3b, 0x0b, 0xbc, 0x24,
0x5e, 0x60, 0xff, 0xb6, 0x0e, 0x1f, 0xdd, 0x9d, 0x25, 0x09, 0xe3, 0x7a, 0x6c, 0x28, 0x20, 0x37, 0x2f, 0x75, 0x3a, 0x93, 0xb1, 0x4f, 0x53, 0x1e, 0x24, 0x5e, 0x92, 0xe5, 0xa5, 0x9e, 0x63, 0xf7,
0x89, 0x2f, 0x99, 0x9a, 0xf8, 0x55, 0xa2, 0x80, 0xff, 0x0d, 0x38, 0xbd, 0x90, 0x71, 0x41, 0xb2, 0x8e, 0x7e, 0x3e, 0xa9, 0x3b, 0x8b, 0x22, 0x14, 0xba, 0x6d, 0x28, 0x20, 0x2f, 0x09, 0x2f, 0x51,
0x90, 0x6d, 0xba, 0x89, 0xbf, 0x1a, 0x1c, 0x3f, 0x2a, 0x22, 0xc0, 0xf5, 0x72, 0x44, 0x54, 0xaf, 0x75, 0xfc, 0x2a, 0x53, 0xc0, 0xfd, 0x06, 0xac, 0x9e, 0x8f, 0x22, 0x61, 0xa9, 0x8f, 0x9b, 0x26,
0x8c, 0x88, 0x07, 0x34, 0xa1, 0x87, 0x7d, 0xd9, 0xe7, 0x55, 0xa2, 0x91, 0xff, 0x93, 0x05, 0x35, 0xf1, 0x57, 0x83, 0xe3, 0x27, 0xf9, 0x0b, 0xe8, 0xbc, 0x6c, 0x11, 0xd5, 0x2b, 0x2d, 0xe2, 0x11,
0x9c, 0x45, 0x86, 0xeb, 0xda, 0xf3, 0xe6, 0xd8, 0x09, 0x8f, 0x2f, 0x82, 0x31, 0xe3, 0xc5, 0xe1, 0x8f, 0xf8, 0x61, 0x5f, 0xe6, 0x79, 0x95, 0x69, 0xe4, 0xfe, 0x64, 0x40, 0x8d, 0x7a, 0x51, 0x49,
0x0a, 0x2c, 0x93, 0x3e, 0x9a, 0xb1, 0xf2, 0xc2, 0xd7, 0x08, 0x7b, 0x0d, 0x7f, 0x0f, 0x0a, 0x2e, 0x75, 0xed, 0x65, 0x7d, 0xec, 0x44, 0x84, 0x17, 0xde, 0x18, 0x45, 0x6e, 0x5c, 0x8e, 0xa5, 0xd3,
0x19, 0xbd, 0x86, 0x62, 0xa2, 0x94, 0xf8, 0x70, 0x1b, 0x64, 0x09, 0xe3, 0xbd, 0xf1, 0x3c, 0x28, 0x47, 0x33, 0x2c, 0x06, 0xbe, 0x46, 0x94, 0x6b, 0xf4, 0xff, 0x90, 0xd7, 0x52, 0x29, 0xd7, 0x88,
0x1e, 0x39, 0x86, 0xc4, 0xbf, 0xab, 0x7e, 0x38, 0xd6, 0x26, 0x9a, 0xb5, 0xf9, 0xe7, 0xe4, 0x6a, 0xcc, 0x14, 0x93, 0x36, 0xbb, 0x41, 0x1a, 0xa1, 0xe8, 0x8d, 0xe7, 0x5e, 0xbe, 0x01, 0x95, 0x28,
0xe4, 0xfe, 0xcf, 0x16, 0x34, 0x1e, 0xea, 0xd7, 0x98, 0x79, 0x0a, 0xeb, 0x99, 0xa7, 0xa8, 0xac, 0xee, 0x7d, 0xf5, 0x47, 0xb2, 0xd6, 0xd1, 0x8c, 0xcd, 0x7f, 0x2f, 0x57, 0x5f, 0xee, 0xfe, 0x6c,
0x9c, 0xa2, 0x0b, 0xbb, 0x85, 0xcd, 0xca, 0xfe, 0x2a, 0x0b, 0x1b, 0x75, 0x3a, 0xa3, 0xb5, 0xb2, 0x40, 0xe3, 0xb1, 0xde, 0xd5, 0xca, 0x56, 0x18, 0x2f, 0xb4, 0xa2, 0xb2, 0x62, 0x45, 0x17, 0x76,
0x58, 0x2f, 0xf2, 0x37, 0x72, 0xba, 0x6a, 0xb3, 0xa9, 0xe0, 0x6b, 0x55, 0x69, 0x43, 0x4b, 0xff, 0x73, 0x99, 0x95, 0xfb, 0x95, 0x17, 0x36, 0xf2, 0xb4, 0x47, 0x6b, 0x45, 0xb0, 0x5e, 0xe5, 0x77,
0x45, 0xca, 0x7f, 0x32, 0x3d, 0x54, 0x0d, 0x91, 0xdf, 0x85, 0xfa, 0x41, 0x1c, 0x4d, 0x82, 0xa9, 0xe5, 0x74, 0x55, 0x66, 0x53, 0xc0, 0xd7, 0xa2, 0xd2, 0x86, 0x96, 0xfe, 0xcd, 0x94, 0x3f, 0x6d,
0xdb, 0x81, 0x5a, 0x2f, 0x13, 0x33, 0xe9, 0xb1, 0xd5, 0xdd, 0x35, 0x88, 0x9f, 0x89, 0x99, 0xb2, 0xba, 0xa9, 0x96, 0x48, 0x6e, 0x17, 0xea, 0x07, 0x61, 0x30, 0xf1, 0xa6, 0x76, 0x07, 0x6a, 0xbd,
0x21, 0xd2, 0xc2, 0xff, 0x0c, 0x60, 0x29, 0xc3, 0x5b, 0x62, 0x59, 0x8d, 0x47, 0xec, 0x12, 0x5b, 0x34, 0x99, 0x49, 0x8d, 0xad, 0xee, 0x6e, 0xa9, 0xf0, 0xd3, 0x64, 0xa6, 0x64, 0x98, 0x94, 0x70,
0x26, 0x95, 0x5e, 0x9a, 0x64, 0x83, 0xc6, 0xff, 0x1c, 0x9c, 0xfd, 0x2c, 0x08, 0xc7, 0x87, 0xd1, 0x3f, 0x03, 0x58, 0xd2, 0x68, 0x4a, 0x2c, 0xa3, 0xf1, 0x04, 0x2f, 0x29, 0x65, 0x62, 0xa9, 0xa5,
0x24, 0xc6, 0xd1, 0x71, 0xce, 0x78, 0xba, 0xac, 0x57, 0x01, 0x31, 0xdd, 0x38, 0x45, 0x4a, 0x0e, 0xc9, 0x36, 0x70, 0xdc, 0xcf, 0xc1, 0xda, 0x4f, 0x3d, 0x7f, 0x7c, 0x18, 0x4c, 0x42, 0x6a, 0x1d,
0x69, 0x34, 0xac, 0xcb, 0xbf, 0xf8, 0xdb, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc4, 0x7e, 0x5e, 0xe7, 0x28, 0xe2, 0x65, 0xbc, 0x72, 0x48, 0xee, 0xa6, 0x2e, 0x52, 0xd4, 0x90, 0x46, 0xc3, 0xba,
0x7e, 0xd7, 0x0f, 0x00, 0x00, 0xfc, 0xcd, 0xbf, 0xf3, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xda, 0x7c, 0x0d, 0xab, 0xf8, 0x0f,
0x00, 0x00,
} }

View File

@ -43,15 +43,16 @@ message DashboardCell {
message TableOptions { message TableOptions {
string timeFormat = 1; // format for time string timeFormat = 1; // format for time
bool verticalTimeAxis = 2; // time axis should be a column not row bool verticalTimeAxis = 2; // time axis should be a column not row
TableColumn sortBy = 3; // which column should a table be sorted by RenamableField sortBy = 3; // which column should a table be sorted by
string wrapping = 4; // option for text wrapping string wrapping = 4; // option for text wrapping
repeated TableColumn columnNames = 5; // names and renames for columns repeated RenamableField fieldNames = 5; // names and renames for column/row fields
bool fixFirstColumn = 6; // first column should be fixed/frozen bool fixFirstColumn = 6; // first column should be fixed/frozen
} }
message TableColumn { message RenamableField {
string internalName = 1; // name of column string internalName = 1; // name of column
string displayName = 2; // what column is renamed to string displayName = 2; // what column is renamed to
bool visible = 3; // Represents whether RenamableField is visible
} }
message Color { message Color {

View File

@ -195,8 +195,8 @@ func Test_MarshalDashboard(t *testing.T) {
}, },
}, },
TableOptions: chronograf.TableOptions{ TableOptions: chronograf.TableOptions{
TimeFormat: "", TimeFormat: "",
ColumnNames: []chronograf.TableColumn{}, FieldNames: []chronograf.RenamableField{},
}, },
}, },
}, },
@ -318,8 +318,8 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Orientation: "bottom", Orientation: "bottom",
}, },
TableOptions: chronograf.TableOptions{ TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY", TimeFormat: "MM:DD:YYYY",
ColumnNames: []chronograf.TableColumn{}, FieldNames: []chronograf.RenamableField{},
}, },
Type: "line", Type: "line",
}, },
@ -434,8 +434,8 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
}, },
}, },
TableOptions: chronograf.TableOptions{ TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY", TimeFormat: "MM:DD:YYYY",
ColumnNames: []chronograf.TableColumn{}, FieldNames: []chronograf.RenamableField{},
}, },
Type: "line", Type: "line",
}, },
@ -474,7 +474,7 @@ func Test_MarshalDashboard_WithEmptyCellType(t *testing.T) {
Axes: map[string]chronograf.Axis{}, Axes: map[string]chronograf.Axis{},
CellColors: []chronograf.CellColor{}, CellColors: []chronograf.CellColor{},
TableOptions: chronograf.TableOptions{ TableOptions: chronograf.TableOptions{
ColumnNames: []chronograf.TableColumn{}, FieldNames: []chronograf.RenamableField{},
}, },
}, },
}, },

View File

@ -560,20 +560,21 @@ type DashboardCell struct {
TableOptions TableOptions `json:"tableOptions,omitempty"` TableOptions TableOptions `json:"tableOptions,omitempty"`
} }
// TableColumn is a column in a DashboardCell of type Table // RenamableField is a column/row field in a DashboardCell of type Table
type TableColumn struct { type RenamableField struct {
InternalName string `json:"internalName"` InternalName string `json:"internalName"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
Visible bool `json:"visible"`
} }
// TableOptions is a type of options for a DashboardCell with type Table // TableOptions is a type of options for a DashboardCell with type Table
type TableOptions struct { type TableOptions struct {
TimeFormat string `json:"timeFormat"` TimeFormat string `json:"timeFormat"`
VerticalTimeAxis bool `json:"verticalTimeAxis"` VerticalTimeAxis bool `json:"verticalTimeAxis"`
SortBy TableColumn `json:"sortBy"` SortBy RenamableField `json:"sortBy"`
Wrapping string `json:"wrapping"` Wrapping string `json:"wrapping"`
ColumnNames []TableColumn `json:"columnNames"` FieldNames []RenamableField `json:"fieldNames"`
FixFirstColumn bool `json:"fixFirstColumn"` FixFirstColumn bool `json:"fixFirstColumn"`
} }
// DashboardsStore is the storage and retrieval of dashboards // DashboardsStore is the storage and retrieval of dashboards

View File

@ -548,9 +548,11 @@ func TestServer(t *testing.T) {
"verticalTimeAxis": false, "verticalTimeAxis": false,
"sortBy":{ "sortBy":{
"internalName": "", "internalName": "",
"displayName": ""}, "displayName": "",
"visible": false
},
"wrapping": "", "wrapping": "",
"columnNames": null, "fieldNames": null,
"fixFirstColumn": false "fixFirstColumn": false
}, },
"links": { "links": {
@ -797,10 +799,11 @@ func TestServer(t *testing.T) {
"verticalTimeAxis":false, "verticalTimeAxis":false,
"sortBy":{ "sortBy":{
"internalName":"", "internalName":"",
"displayName":"" "displayName":"",
"visible":false
}, },
"wrapping":"", "wrapping":"",
"columnNames":null, "fieldNames":null,
"fixFirstColumn":false "fixFirstColumn":false
}, },
"legend":{ "legend":{

View File

@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
} }
} }
`))), `))),
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"tableOptions":{"timeFormat":"","verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":""},"wrapping":"","columnNames":null,"fixFirstColumn":false},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}} want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"tableOptions":{"timeFormat":"","verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fieldNames":null,"fixFirstColumn":false},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
`, `,
}, },
{ {

View File

@ -48,6 +48,7 @@ class CellEditorOverlay extends Component {
activeQueryIndex: 0, activeQueryIndex: 0,
isDisplayOptionsTabActive: false, isDisplayOptionsTabActive: false,
staticLegend: IS_STATIC_LEGEND(legend), staticLegend: IS_STATIC_LEGEND(legend),
dataLabels: [],
} }
} }
@ -259,6 +260,10 @@ class CellEditorOverlay extends Component {
this.overlayRef.focus() this.overlayRef.focus()
} }
setDataLabels = dataLabels => {
this.setState({dataLabels})
}
render() { render() {
const { const {
onCancel, onCancel,
@ -273,6 +278,7 @@ class CellEditorOverlay extends Component {
isDisplayOptionsTabActive, isDisplayOptionsTabActive,
queriesWorkingDraft, queriesWorkingDraft,
staticLegend, staticLegend,
dataLabels,
} = this.state } = this.state
const queryActions = { const queryActions = {
@ -305,6 +311,7 @@ class CellEditorOverlay extends Component {
queryConfigs={queriesWorkingDraft} queryConfigs={queriesWorkingDraft}
editQueryStatus={editQueryStatus} editQueryStatus={editQueryStatus}
staticLegend={staticLegend} staticLegend={staticLegend}
setDataLabels={this.setDataLabels}
/> />
<CEOBottom> <CEOBottom>
<OverlayControls <OverlayControls
@ -324,6 +331,7 @@ class CellEditorOverlay extends Component {
onToggleStaticLegend={this.handleToggleStaticLegend} onToggleStaticLegend={this.handleToggleStaticLegend}
staticLegend={staticLegend} staticLegend={staticLegend}
onResetFocus={this.handleResetFocus} onResetFocus={this.handleResetFocus}
dataLabels={dataLabels}
/> />
) : ( ) : (
<QueryMaker <QueryMaker

View File

@ -42,7 +42,7 @@ class DisplayOptions extends Component {
staticLegend, staticLegend,
onToggleStaticLegend, onToggleStaticLegend,
onResetFocus, onResetFocus,
queryConfigs, dataLabels,
} = this.props } = this.props
switch (type) { switch (type) {
case 'gauge': case 'gauge':
@ -51,10 +51,7 @@ class DisplayOptions extends Component {
return <SingleStatOptions onResetFocus={onResetFocus} /> return <SingleStatOptions onResetFocus={onResetFocus} />
case 'table': case 'table':
return ( return (
<TableOptions <TableOptions onResetFocus={onResetFocus} dataLabels={dataLabels} />
onResetFocus={onResetFocus}
queryConfigs={queryConfigs}
/>
) )
default: default:
return ( return (
@ -93,6 +90,7 @@ DisplayOptions.propTypes = {
onToggleStaticLegend: func.isRequired, onToggleStaticLegend: func.isRequired,
staticLegend: bool, staticLegend: bool,
onResetFocus: func.isRequired, onResetFocus: func.isRequired,
dataLabels: arrayOf(string),
} }
const mapStateToProps = ({cellEditorOverlay: {cell, cell: {axes}}}) => ({ const mapStateToProps = ({cellEditorOverlay: {cell, cell: {axes}}}) => ({

View File

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

View File

@ -0,0 +1,65 @@
import React, {PureComponent} from 'react'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
interface Field {
internalName: string
displayName: string
visible: boolean
}
interface Props {
internalName: string
displayName: string
visible: boolean
onFieldUpdate: (field: Field) => void
}
class GraphOptionsCustomizableField extends PureComponent<Props, {}> {
constructor(props) {
super(props)
this.handleFieldRename = this.handleFieldRename.bind(this)
this.handleToggleVisible = this.handleToggleVisible.bind(this)
}
public handleFieldRename(rename: string) {
const {onFieldUpdate, internalName, visible} = this.props
onFieldUpdate({internalName, displayName: rename, visible})
}
public handleToggleVisible() {
const {onFieldUpdate, internalName, displayName, visible} = this.props
onFieldUpdate({internalName, displayName, visible: !visible})
}
public render() {
const {internalName, displayName, visible} = this.props
return (
<div className="field-controls--section">
<div
className={
visible ? 'field-controls--label' : 'field-controls--label-hidden'
}
>
<span
className={visible ? 'icon eye-open' : 'icon eye-closed'}
onClick={this.handleToggleVisible}
/>
{internalName}
</div>
<InputClickToEdit
value={displayName}
wrapperClass="field-controls-input"
onBlur={this.handleFieldRename}
placeholder="Rename..."
appearAsNormalInput={true}
disabled={!visible}
/>
</div>
)
}
}
export default GraphOptionsCustomizableField

View File

@ -1,37 +0,0 @@
import React, {SFC} from 'react'
import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn'
import uuid from 'uuid'
interface Column {
internalName: string
displayName: string
}
interface Props {
columns: Column[]
onColumnRename: (column: Column) => void
}
const GraphOptionsCustomizeColumns: SFC<Props> = ({
columns,
onColumnRename,
}) => {
return (
<div className="graph-options-group">
<label className="form-label">Customize Columns</label>
{columns.map(col => {
return (
<GraphOptionsCustomizableColumn
key={uuid.v4()}
internalName={col.internalName}
displayName={col.displayName}
onColumnRename={onColumnRename}
/>
)
})}
</div>
)
}
export default GraphOptionsCustomizeColumns

View File

@ -0,0 +1,38 @@
import React, {SFC} from 'react'
import GraphOptionsCustomizableField from 'src/dashboards/components/GraphOptionsCustomizableField'
import uuid from 'uuid'
interface Field {
internalName: string
displayName: string
visible: boolean
}
interface Props {
fields: Field[]
onFieldUpdate: (field: Field) => void
}
const GraphOptionsCustomizeFields: SFC<Props> = ({fields, onFieldUpdate}) => {
return (
<div className="graph-options-group">
<label className="form-label">Customize Fields</label>
<div className="field-controls--group">
{fields.map(field => {
return (
<GraphOptionsCustomizableField
key={uuid.v4()}
internalName={field.internalName}
displayName={field.displayName}
visible={field.visible}
onFieldUpdate={onFieldUpdate}
/>
)
})}
</div>
</div>
)
}
export default GraphOptionsCustomizeFields

View File

@ -23,10 +23,9 @@ const GraphOptionsSortBy = ({
selected, selected,
}: Props) => { }: Props) => {
const selectedValue = selected.displayName || selected.internalName const selectedValue = selected.displayName || selected.internalName
return ( return (
<div className="form-group col-xs-6"> <div className="form-group col-xs-6">
<label>Sort By</label> <label>Default Sort By</label>
<Dropdown <Dropdown
items={sortByOptions} items={sortByOptions}
selected={selectedValue} selected={selectedValue}

View File

@ -35,18 +35,22 @@ class GraphOptionsTimeFormat extends PureComponent<Props, State> {
return this.props.onTimeFormatChange return this.props.onTimeFormatChange
} }
public handleChangeFormat = format => {
this.onTimeFormatChange(format)
this.setState({format})
}
public handleChooseFormat = (formatOption: TimeFormatOptions) => { public handleChooseFormat = (formatOption: TimeFormatOptions) => {
if (formatOption.text === TIME_FORMAT_CUSTOM) { if (formatOption.text === TIME_FORMAT_CUSTOM) {
this.setState({customFormat: true}) this.setState({customFormat: true})
} else { } else {
this.setState({format: formatOption.text, customFormat: false})
this.onTimeFormatChange(formatOption.text) this.onTimeFormatChange(formatOption.text)
this.setState({format: formatOption.text, customFormat: false})
} }
} }
public render() { public render() {
const {format, customFormat} = this.state const {format, customFormat} = this.state
const {onTimeFormatChange} = this.props
const tipContent = const tipContent =
'For information on formatting, see http://momentjs.com/docs/#/parsing/string-format/' 'For information on formatting, see http://momentjs.com/docs/#/parsing/string-format/'
@ -57,7 +61,7 @@ class GraphOptionsTimeFormat extends PureComponent<Props, State> {
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label> <label>
Time Format Time Format
{customFormat && ( {showCustom && (
<QuestionMarkTooltip <QuestionMarkTooltip
tipID="Time Axis Format" tipID="Time Axis Format"
tipContent={tipContent} tipContent={tipContent}
@ -75,10 +79,10 @@ class GraphOptionsTimeFormat extends PureComponent<Props, State> {
{showCustom && ( {showCustom && (
<div className="column-controls--section"> <div className="column-controls--section">
<InputClickToEdit <InputClickToEdit
wrapperClass="column-controls-input " wrapperClass="field-controls-input "
value={format} value={format}
onBlur={onTimeFormatChange} onBlur={this.handleChangeFormat}
onChange={onTimeFormatChange} onChange={this.handleChangeFormat}
placeholder="Enter custom format..." placeholder="Enter custom format..."
appearAsNormalInput={true} appearAsNormalInput={true}
/> />

View File

@ -2,54 +2,45 @@ import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import _ from 'lodash' import GraphOptionsCustomizeFields from 'src/dashboards/components/GraphOptionsCustomizeFields'
import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns'
import GraphOptionsFixFirstColumn from 'src/dashboards/components/GraphOptionsFixFirstColumn' import GraphOptionsFixFirstColumn from 'src/dashboards/components/GraphOptionsFixFirstColumn'
import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy' import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy'
import GraphOptionsTextWrapping from 'src/dashboards/components/GraphOptionsTextWrapping'
import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis' import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis'
import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import _ from 'lodash'
import ThresholdsList from 'src/shared/components/ThresholdsList' import ThresholdsList from 'src/shared/components/ThresholdsList'
import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle' import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle'
import {updateTableOptions} from 'src/dashboards/actions/cellEditorOverlay' import {updateTableOptions} from 'src/dashboards/actions/cellEditorOverlay'
import {TIME_COLUMN_DEFAULT} from 'src/shared/constants/tableGraph' import {TIME_FIELD_DEFAULT} from 'src/shared/constants/tableGraph'
interface Option { interface Option {
text: string text: string
key: string key: string
} }
interface TableColumn { interface RenamableField {
internalName: string internalName: string
displayName: string displayName: string
visible: boolean
} }
interface Options { interface Options {
timeFormat: string timeFormat: string
verticalTimeAxis: boolean verticalTimeAxis: boolean
sortBy: TableColumn sortBy: RenamableField
wrapping: string fieldNames: RenamableField[]
columnNames: TableColumn[]
fixFirstColumn: boolean fixFirstColumn: boolean
} }
interface QueryConfig {
measurement: string
fields: Array<{
alias: string
value: string
}>
}
interface Props { interface Props {
queryConfigs: QueryConfig[]
handleUpdateTableOptions: (options: Options) => void handleUpdateTableOptions: (options: Options) => void
tableOptions: Options tableOptions: Options
onResetFocus: () => void onResetFocus: () => void
dataLabels: string[]
} }
export class TableOptions extends PureComponent<Props, {}> { export class TableOptions extends PureComponent<Props, {}> {
@ -57,47 +48,33 @@ export class TableOptions extends PureComponent<Props, {}> {
super(props) super(props)
} }
get columnNames() { get fieldNames() {
const {tableOptions: {columnNames}} = this.props const {tableOptions: {fieldNames}} = this.props
return columnNames || [] return fieldNames || []
} }
get timeColumn() { get timeColumn() {
return ( return (
this.columnNames.find(c => c.internalName === 'time') || this.fieldNames.find(f => f.internalName === 'time') || TIME_FIELD_DEFAULT
TIME_COLUMN_DEFAULT
) )
} }
get computedColumnNames() { get computedFieldNames() {
const {queryConfigs} = this.props const {dataLabels} = this.props
const queryFields = _.flatten( return dataLabels.map(label => {
queryConfigs.map(({measurement, fields}) => { const existing = this.fieldNames.find(f => f.internalName === label)
return fields.map(({alias}) => { return existing || {internalName: label, displayName: '', visible: true}
const internalName = `${measurement}.${alias}`
const existing = this.columnNames.find(
c => c.internalName === internalName
)
return existing || {internalName, displayName: ''}
})
})
)
return [this.timeColumn, ...queryFields]
}
public componentWillMount() {
const {handleUpdateTableOptions, tableOptions} = this.props
handleUpdateTableOptions({
...tableOptions,
columnNames: this.computedColumnNames,
}) })
} }
public handleChooseSortBy = (option: Option) => { public handleChooseSortBy = (option: Option) => {
const {tableOptions, handleUpdateTableOptions} = this.props const {tableOptions, handleUpdateTableOptions} = this.props
const sortBy = {displayName: option.text, internalName: option.key} const sortBy = {
displayName: option.text === option.key ? '' : option.text,
internalName: option.key,
visible: true,
}
handleUpdateTableOptions({...tableOptions, sortBy}) handleUpdateTableOptions({...tableOptions, sortBy})
} }
@ -118,32 +95,55 @@ export class TableOptions extends PureComponent<Props, {}> {
handleUpdateTableOptions({...tableOptions, fixFirstColumn}) handleUpdateTableOptions({...tableOptions, fixFirstColumn})
} }
public handleColumnRename = column => { public handleFieldUpdate = field => {
const {handleUpdateTableOptions, tableOptions} = this.props const {handleUpdateTableOptions, tableOptions} = this.props
const {columnNames} = tableOptions const {fieldNames, sortBy} = tableOptions
const updatedColumns = columnNames.map( const updatedFields = fieldNames.map(
op => (op.internalName === column.internalName ? column : op) f => (f.internalName === field.internalName ? field : f)
) )
handleUpdateTableOptions({...tableOptions, columnNames: updatedColumns}) const updatedSortBy =
sortBy.internalName === field.internalName
? {...sortBy, displayName: field.displayName}
: sortBy
handleUpdateTableOptions({
...tableOptions,
fieldNames: updatedFields,
sortBy: updatedSortBy,
})
}
public componentWillMount() {
const {handleUpdateTableOptions, tableOptions} = this.props
handleUpdateTableOptions({
...tableOptions,
fieldNames: this.computedFieldNames,
})
}
public shouldComponentUpdate(nextProps) {
const {tableOptions, dataLabels} = this.props
const tableOptionsDifferent = !_.isEqual(
tableOptions,
nextProps.tableOptions
)
const dataLabelsDifferent = !_.isEqual(dataLabels, nextProps.dataLabels)
return tableOptionsDifferent || dataLabelsDifferent
} }
public handleToggleTextWrapping = () => {} public handleToggleTextWrapping = () => {}
public render() { public render() {
const { const {
tableOptions: { tableOptions: {timeFormat, fieldNames, verticalTimeAxis, fixFirstColumn},
timeFormat,
columnNames: columns,
verticalTimeAxis,
fixFirstColumn,
},
onResetFocus, onResetFocus,
tableOptions, tableOptions,
} = this.props } = this.props
const tableSortByOptions = this.computedColumnNames.map(col => ({ const tableSortByOptions = this.computedFieldNames.map(field => ({
key: col.internalName, key: field.internalName,
text: col.displayName || col.internalName, text: field.displayName || field.internalName,
})) }))
return ( return (
@ -163,22 +163,18 @@ export class TableOptions extends PureComponent<Props, {}> {
onToggleVerticalTimeAxis={this.handleToggleVerticalTimeAxis} onToggleVerticalTimeAxis={this.handleToggleVerticalTimeAxis}
/> />
<GraphOptionsSortBy <GraphOptionsSortBy
selected={tableOptions.sortBy || TIME_COLUMN_DEFAULT} selected={tableOptions.sortBy || TIME_FIELD_DEFAULT}
sortByOptions={tableSortByOptions} sortByOptions={tableSortByOptions}
onChooseSortBy={this.handleChooseSortBy} onChooseSortBy={this.handleChooseSortBy}
/> />
<GraphOptionsTextWrapping
thresholdsListType="background"
onToggleTextWrapping={this.handleToggleTextWrapping}
/>
<GraphOptionsFixFirstColumn <GraphOptionsFixFirstColumn
fixed={fixFirstColumn} fixed={fixFirstColumn}
onToggleFixFirstColumn={this.handleToggleFixFirstColumn} onToggleFixFirstColumn={this.handleToggleFixFirstColumn}
/> />
</div> </div>
<GraphOptionsCustomizeColumns <GraphOptionsCustomizeFields
columns={columns} fields={fieldNames}
onColumnRename={this.handleColumnRename} onFieldUpdate={this.handleFieldUpdate}
/> />
<ThresholdsList showListHeading={true} onResetFocus={onResetFocus} /> <ThresholdsList showListHeading={true} onResetFocus={onResetFocus} />
<div className="form-group-wrapper graph-options-group"> <div className="form-group-wrapper graph-options-group">

View File

@ -22,6 +22,7 @@ const DashVisualization = (
staticLegend, staticLegend,
thresholdsListColors, thresholdsListColors,
tableOptions, tableOptions,
setDataLabels,
}, },
{source: {links: {proxy}}} {source: {links: {proxy}}}
) => { ) => {
@ -42,6 +43,7 @@ const DashVisualization = (
editQueryStatus={editQueryStatus} editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight} resizerTopHeight={resizerTopHeight}
staticLegend={staticLegend} staticLegend={staticLegend}
setDataLabels={setDataLabels}
/> />
</div> </div>
</div> </div>
@ -86,6 +88,7 @@ DashVisualization.propTypes = {
}).isRequired }).isRequired
), ),
staticLegend: bool, staticLegend: bool,
setDataLabels: func,
} }
DashVisualization.contextTypes = { DashVisualization.contextTypes = {

View File

@ -23,6 +23,7 @@ const RefreshingGraph = ({
cellID, cellID,
queries, queries,
tableOptions, tableOptions,
setDataLabels,
templates, templates,
timeRange, timeRange,
cellHeight, cellHeight,
@ -98,6 +99,7 @@ const RefreshingGraph = ({
hoverTime={hoverTime} hoverTime={hoverTime}
onSetHoverTime={onSetHoverTime} onSetHoverTime={onSetHoverTime}
inView={inView} inView={inView}
setDataLabels={setDataLabels}
/> />
) )
} }
@ -164,6 +166,7 @@ RefreshingGraph.propTypes = {
cellID: string, cellID: string,
inView: bool, inView: bool,
tableOptions: shape({}), tableOptions: shape({}),
setDataLabels: func,
} }
RefreshingGraph.defaultProps = { RefreshingGraph.defaultProps = {

View File

@ -2,91 +2,130 @@ import React, {Component} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import _ from 'lodash' import _ from 'lodash'
import classnames from 'classnames' import classnames from 'classnames'
import isEmpty from 'lodash/isEmpty'
import {MultiGrid, ColumnSizer} from 'react-virtualized' import {MultiGrid, ColumnSizer} from 'react-virtualized'
import moment from 'moment' import moment from 'moment'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph' import {timeSeriesToTableGraph} from 'src/utils/timeSeriesToDygraph'
import { import {
NULL_COLUMN_INDEX, NULL_ARRAY_INDEX,
NULL_ROW_INDEX,
NULL_HOVER_TIME, NULL_HOVER_TIME,
TIME_FORMAT_DEFAULT, TIME_FORMAT_DEFAULT,
TIME_COLUMN_DEFAULT, TIME_FIELD_DEFAULT,
ASCENDING, ASCENDING,
DESCENDING, DESCENDING,
FIX_FIRST_COLUMN_DEFAULT, FIX_FIRST_COLUMN_DEFAULT,
VERTICAL_TIME_AXIS_DEFAULT,
calculateTimeColumnWidth, calculateTimeColumnWidth,
} from 'src/shared/constants/tableGraph' } from 'src/shared/constants/tableGraph'
const DEFAULT_SORT = ASCENDING const DEFAULT_SORT = ASCENDING
import {generateThresholdsListHexs} from 'shared/constants/colorOperations' import {generateThresholdsListHexs} from 'shared/constants/colorOperations'
const filterInvisibleColumns = (data, fieldNames) => {
const visibility = {}
const filteredData = data.map((row, i) => {
return row.filter((col, j) => {
if (i === 0) {
const foundField = fieldNames.find(field => field.internalName === col)
visibility[j] = foundField ? foundField.visible : true
}
return visibility[j]
})
})
return filteredData[0].length ? filteredData : [[]]
}
const processData = (
data,
sortFieldName,
direction,
verticalTimeAxis,
fieldNames
) => {
const sortIndex = _.indexOf(data[0], sortFieldName)
const sortedData = [
data[0],
..._.orderBy(_.drop(data, 1), sortIndex, [direction]),
]
const filteredData = filterInvisibleColumns(sortedData, fieldNames)
const processedData = verticalTimeAxis ? filteredData : _.unzip(filteredData)
return {processedData}
}
class TableGraph extends Component { class TableGraph extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
data: [[]], data: [[]],
unzippedData: [[]],
hoveredColumnIndex: NULL_COLUMN_INDEX,
hoveredRowIndex: NULL_ROW_INDEX,
sortByColumnIndex: NULL_COLUMN_INDEX,
clickToSortFieldIndex: NULL_COLUMN_INDEX,
clicktoSortDirection: DEFAULT_SORT,
timeColumnWidth: calculateTimeColumnWidth(props.tableOptions.timeFormat), timeColumnWidth: calculateTimeColumnWidth(props.tableOptions.timeFormat),
processedData: [[]],
hoveredColumnIndex: NULL_ARRAY_INDEX,
hoveredRowIndex: NULL_ARRAY_INDEX,
sortField: '',
sortDirection: DEFAULT_SORT,
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const {data} = timeSeriesToTableGraph(nextProps.data) const {labels, data} = timeSeriesToTableGraph(nextProps.data)
if (_.isEmpty(data[0])) {
return
}
const {sortField, sortDirection} = this.state
const { const {
clickToSortFieldIndex, tableOptions: {
clicktoSortDirection, sortBy: {internalName},
sortByColumnIndex, fieldNames,
} = this.state verticalTimeAxis,
const {tableOptions: {sortBy: {internalName}, timeFormat}} = nextProps timeFormat,
},
setDataLabels,
} = nextProps
if (timeFormat !== this.props.tableOptions.timeFormat) { if (timeFormat !== this.props.tableOptions.timeFormat) {
this.setState({ this.setState({
timeColumnWidth: calculateTimeColumnWidth(timeFormat), timeColumnWidth: calculateTimeColumnWidth(timeFormat),
}) })
} }
if (
sortByColumnIndex === NULL_COLUMN_INDEX || if (setDataLabels) {
_.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') !== setDataLabels(labels)
internalName
) {
const newSortByColumnIndex = _.indexOf(data[0], internalName)
const sortedData = [
data[0],
..._.orderBy(_.drop(data, 1), newSortByColumnIndex, [DEFAULT_SORT]),
]
this.setState({
data: sortedData,
unzippedData: _.unzip(sortedData),
sortByColumnIndex: newSortByColumnIndex,
clickToSortFieldIndex: NULL_COLUMN_INDEX,
clicktoSortDirection: DEFAULT_SORT,
})
return
} }
const clicked = clickToSortFieldIndex !== NULL_COLUMN_INDEX let direction, sortFieldName
const sortIndex = clicked ? clickToSortFieldIndex : sortByColumnIndex if (
const direction = clicked ? clicktoSortDirection : DEFAULT_SORT _.isEmpty(sortField) ||
const sortedData = [ _.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') !==
data[0], _.get(nextProps, ['tableOptions', 'sortBy', 'internalName'], '')
..._.orderBy(_.drop(data, 1), sortIndex, [direction]), ) {
] direction = DEFAULT_SORT
sortFieldName = internalName
} else {
direction = sortDirection
sortFieldName = sortField
}
const {processedData} = processData(
data,
sortFieldName,
direction,
verticalTimeAxis,
fieldNames
)
this.setState({ this.setState({
data: sortedData, data,
unzippedData: _.unzip(sortedData), processedData,
sortField: sortFieldName,
sortDirection: direction,
}) })
} }
calcHoverTimeIndex = (data, hoverTime, verticalTimeAxis) => { calcHoverTimeIndex = (data, hoverTime, verticalTimeAxis) => {
if (isEmpty(data) || hoverTime === NULL_HOVER_TIME) { if (_.isEmpty(data) || hoverTime === NULL_HOVER_TIME) {
return undefined return undefined
} }
if (verticalTimeAxis) { if (verticalTimeAxis) {
@ -99,11 +138,14 @@ class TableGraph extends Component {
handleHover = (columnIndex, rowIndex) => () => { handleHover = (columnIndex, rowIndex) => () => {
const {onSetHoverTime, tableOptions: {verticalTimeAxis}} = this.props const {onSetHoverTime, tableOptions: {verticalTimeAxis}} = this.props
const data = verticalTimeAxis ? this.state.data : this.state.unzippedData const {data} = this.state
if (rowIndex === 0 && verticalTimeAxis) {
return
}
if (onSetHoverTime) { if (onSetHoverTime) {
const hoverTime = verticalTimeAxis const hoverTime = verticalTimeAxis
? data[rowIndex][0] ? data[rowIndex][0]
: data[0][columnIndex] : data[columnIndex][0]
onSetHoverTime(hoverTime.toString()) onSetHoverTime(hoverTime.toString())
} }
this.setState({ this.setState({
@ -116,42 +158,37 @@ class TableGraph extends Component {
if (this.props.onSetHoverTime) { if (this.props.onSetHoverTime) {
this.props.onSetHoverTime(NULL_HOVER_TIME) this.props.onSetHoverTime(NULL_HOVER_TIME)
this.setState({ this.setState({
hoveredColumnIndex: NULL_COLUMN_INDEX, hoveredColumnIndex: NULL_ARRAY_INDEX,
hoveredRowIndex: NULL_ROW_INDEX, hoveredRowIndex: NULL_ARRAY_INDEX,
}) })
} }
} }
handleClickFieldName = (columnIndex, rowIndex) => () => { handleClickFieldName = fieldName => () => {
const {tableOptions} = this.props const {tableOptions} = this.props
const {clickToSortFieldIndex, clicktoSortDirection, data} = this.state const {data, sortField, sortDirection} = this.state
const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true) const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true)
const newIndex = verticalTimeAxis ? columnIndex : rowIndex const fieldNames = _.get(tableOptions, 'fieldNames', [TIME_FIELD_DEFAULT])
if (clickToSortFieldIndex === newIndex) { let direction
const direction = if (fieldName === sortField) {
clicktoSortDirection === ASCENDING ? DESCENDING : ASCENDING direction = sortDirection === ASCENDING ? DESCENDING : ASCENDING
const sortedData = [ } else {
data[0], direction = DEFAULT_SORT
..._.orderBy(_.drop(data, 1), clickToSortFieldIndex, [direction]),
]
this.setState({
data: sortedData,
unzippedData: _.unzip(sortedData),
clicktoSortDirection: direction,
})
return
} }
const sortedData = [ const {processedData} = processData(
data[0], data,
..._.orderBy(_.drop(data, 1), clickToSortFieldIndex, [DEFAULT_SORT]), fieldName,
] direction,
verticalTimeAxis,
fieldNames
)
this.setState({ this.setState({
data: sortedData, processedData,
unzippedData: _.unzip(sortedData), sortField: fieldName,
clickToSortFieldIndex: newIndex, sortDirection: direction,
clicktoSortDirection: DEFAULT_SORT,
}) })
} }
@ -159,9 +196,8 @@ class TableGraph extends Component {
const {index} = column const {index} = column
const {tableOptions, tableOptions: {verticalTimeAxis}} = this.props const {tableOptions, tableOptions: {verticalTimeAxis}} = this.props
const {timeColumnWidth} = this.state const {timeColumnWidth} = this.state
const columnNames = _.get(tableOptions, 'columnNames', [
TIME_COLUMN_DEFAULT, const columnNames = _.get(tableOptions, 'columnNames', [TIME_FIELD_DEFAULT])
])
return verticalTimeAxis && columnNames[index].internalName === 'time' return verticalTimeAxis && columnNames[index].internalName === 'time'
? timeColumnWidth ? timeColumnWidth
@ -169,28 +205,31 @@ class TableGraph extends Component {
} }
cellRenderer = ({columnIndex, rowIndex, key, parent, style}) => { cellRenderer = ({columnIndex, rowIndex, key, parent, style}) => {
const {hoveredColumnIndex, hoveredRowIndex} = this.state const {hoveredColumnIndex, hoveredRowIndex, processedData} = this.state
const {tableOptions, colors} = this.props const {tableOptions, colors} = this.props
const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true)
const data = verticalTimeAxis ? this.state.data : this.state.unzippedData const {
const timeFormat = _.get(tableOptions, 'timeFormat', TIME_FORMAT_DEFAULT) timeFormat = TIME_FORMAT_DEFAULT,
const columnNames = _.get(tableOptions, 'columnNames', [ verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
TIME_COLUMN_DEFAULT, fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
]) fieldNames = [TIME_FIELD_DEFAULT],
const fixFirstColumn = _.get( } = tableOptions
tableOptions,
'fixFirstColumn', const cellData = processedData[rowIndex][columnIndex]
FIX_FIRST_COLUMN_DEFAULT
const timeField = fieldNames.find(
field => field.internalName === TIME_FIELD_DEFAULT.internalName
) )
const visibleTime = _.get(timeField, 'visible', true)
const isFixedRow = rowIndex === 0 && columnIndex > 0 const isFixedRow = rowIndex === 0 && columnIndex > 0
const isFixedColumn = fixFirstColumn && rowIndex > 0 && columnIndex === 0 const isFixedColumn = fixFirstColumn && rowIndex > 0 && columnIndex === 0
const isTimeData = verticalTimeAxis const isTimeData =
? rowIndex > 0 && columnIndex === 0 visibleTime &&
: isFixedRow (verticalTimeAxis ? rowIndex > 0 && columnIndex === 0 : isFixedRow)
const isFieldName = verticalTimeAxis ? rowIndex === 0 : columnIndex === 0 const isFieldName = verticalTimeAxis ? rowIndex === 0 : columnIndex === 0
const isFixedCorner = rowIndex === 0 && columnIndex === 0 const isFixedCorner = rowIndex === 0 && columnIndex === 0
const dataIsNumerical = _.isNumber(data[rowIndex][columnIndex]) const dataIsNumerical = _.isNumber(cellData)
const isHighlightedRow = const isHighlightedRow =
rowIndex === parent.props.scrollToRow || rowIndex === parent.props.scrollToRow ||
(rowIndex === hoveredRowIndex && hoveredRowIndex !== 0) (rowIndex === hoveredRowIndex && hoveredRowIndex !== 0)
@ -203,7 +242,7 @@ class TableGraph extends Component {
if (!isFixedRow && !isFixedColumn && !isFixedCorner) { if (!isFixedRow && !isFixedColumn && !isFixedCorner) {
const {bgColor, textColor} = generateThresholdsListHexs({ const {bgColor, textColor} = generateThresholdsListHexs({
colors, colors,
lastValue: data[rowIndex][columnIndex], lastValue: cellData,
cellType: 'table', cellType: 'table',
}) })
@ -224,25 +263,21 @@ class TableGraph extends Component {
'table-graph-cell__isFieldName': isFieldName, 'table-graph-cell__isFieldName': isFieldName,
}) })
const cellData = data[rowIndex][columnIndex] const foundField =
const foundColumn = columnNames.find( isFieldName && fieldNames.find(field => field.internalName === cellData)
column => column.internalName === cellData const fieldName =
) foundField && (foundField.displayName || foundField.internalName)
const columnName =
foundColumn && (foundColumn.displayName || foundColumn.internalName)
const cellContents = isTimeData const cellContents = isTimeData
? `${moment(cellData).format(timeFormat)}` ? `${moment(cellData).format(timeFormat)}`
: columnName || `${cellData}` : fieldName || `${cellData}`
return ( return (
<div <div
key={key} key={key}
style={cellStyle} style={cellStyle}
className={cellClass} className={cellClass}
onClick={ onClick={isFieldName ? this.handleClickFieldName(cellData) : null}
isFieldName ? this.handleClickFieldName(columnIndex, rowIndex) : null
}
onMouseOver={this.handleHover(columnIndex, rowIndex)} onMouseOver={this.handleHover(columnIndex, rowIndex)}
title={cellContents} title={cellContents}
> >
@ -253,33 +288,41 @@ class TableGraph extends Component {
render() { render() {
const { const {
sortByColumnIndex,
clickToSortFieldIndex,
clicktoSortDirection,
hoveredColumnIndex, hoveredColumnIndex,
hoveredRowIndex, hoveredRowIndex,
sortField,
sortDirection,
processedData,
data,
} = this.state } = this.state
const {hoverTime, tableOptions, colors} = this.props const {hoverTime, tableOptions, colors} = this.props
const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true) const {
const data = verticalTimeAxis ? this.state.data : this.state.unzippedData verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT,
fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT,
} = tableOptions
const columnCount = _.get(processedData, ['0', 'length'], 0)
const rowCount = columnCount === 0 ? 0 : processedData.length
const columnCount = _.get(data, ['0', 'length'], 0)
const rowCount = data.length
const COLUMN_MIN_WIDTH = 98 const COLUMN_MIN_WIDTH = 98
const COLUMN_MAX_WIDTH = 1000 const COLUMN_MAX_WIDTH = 1000
const ROW_HEIGHT = 30 const ROW_HEIGHT = 30
const fixedColumnCount = fixFirstColumn && columnCount > 1 ? 1 : undefined
const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0) const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0)
const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0) const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0)
const hoverTimeIndex = const hoverTimeIndex =
hoveredRowIndex === NULL_ROW_INDEX hoveredRowIndex === NULL_ARRAY_INDEX
? this.calcHoverTimeIndex(data, hoverTime, verticalTimeAxis) ? this.calcHoverTimeIndex(data, hoverTime, verticalTimeAxis)
: hoveredRowIndex : hoveredRowIndex
const fixedColumnCount = tableOptions.fixFirstColumn ? 1 : undefined const hoveringThisTable = hoveredColumnIndex !== NULL_ARRAY_INDEX
const hoveringThisTable = hoveredColumnIndex !== NULL_COLUMN_INDEX
const scrollToRow =
!hoveringThisTable && verticalTimeAxis ? hoverTimeIndex : undefined
const scrollToColumn = const scrollToColumn =
!hoveringThisTable && !verticalTimeAxis ? hoverTimeIndex : undefined !hoveringThisTable && !verticalTimeAxis ? hoverTimeIndex : undefined
const scrollToRow =
!hoveringThisTable && verticalTimeAxis ? hoverTimeIndex : undefined
return ( return (
<div <div
@ -287,14 +330,14 @@ class TableGraph extends Component {
ref={gridContainer => (this.gridContainer = gridContainer)} ref={gridContainer => (this.gridContainer = gridContainer)}
onMouseOut={this.handleMouseOut} onMouseOut={this.handleMouseOut}
> >
{!isEmpty(data) && ( {rowCount > 0 &&
<ColumnSizer <ColumnSizer
columnCount={columnCount} columnCount={columnCount}
columnMaxWidth={COLUMN_MAX_WIDTH} columnMaxWidth={COLUMN_MAX_WIDTH}
columnMinWidth={COLUMN_MIN_WIDTH} columnMinWidth={COLUMN_MIN_WIDTH}
width={tableWidth} width={tableWidth}
> >
{({getColumnWidth, registerChild}) => ( {({getColumnWidth, registerChild}) =>
<MultiGrid <MultiGrid
ref={registerChild} ref={registerChild}
columnCount={columnCount} columnCount={columnCount}
@ -307,41 +350,47 @@ class TableGraph extends Component {
fixedRowCount={1} fixedRowCount={1}
enableFixedColumnScroll={true} enableFixedColumnScroll={true}
enableFixedRowScroll={true} enableFixedRowScroll={true}
timeFormat={
tableOptions ? tableOptions.timeFormat : TIME_FORMAT_DEFAULT
}
columnNames={
tableOptions
? tableOptions.columnNames
: [TIME_COLUMN_DEFAULT]
}
scrollToRow={scrollToRow} scrollToRow={scrollToRow}
scrollToColumn={scrollToColumn} scrollToColumn={scrollToColumn}
verticalTimeAxis={verticalTimeAxis} sortField={sortField}
sortByColumnIndex={sortByColumnIndex} sortDirection={sortDirection}
clickToSortFieldIndex={clickToSortFieldIndex}
clicktoSortDirection={clicktoSortDirection}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
hoveredColumnIndex={hoveredColumnIndex} hoveredColumnIndex={hoveredColumnIndex}
hoveredRowIndex={hoveredRowIndex} hoveredRowIndex={hoveredRowIndex}
hoverTime={hoverTime} hoverTime={hoverTime}
colors={colors} colors={colors}
tableOptions={tableOptions}
classNameBottomRightGrid="table-graph--scroll-window" classNameBottomRightGrid="table-graph--scroll-window"
/> />}
)} </ColumnSizer>}
</ColumnSizer>
)}
</div> </div>
) )
} }
} }
const {arrayOf, number, shape, string, func} = PropTypes const {arrayOf, bool, number, shape, string, func} = PropTypes
TableGraph.propTypes = { TableGraph.propTypes = {
cellHeight: number, cellHeight: number,
data: arrayOf(shape()), data: arrayOf(shape()),
tableOptions: shape({}), tableOptions: shape({
timeFormat: string.isRequired,
verticalTimeAxis: bool.isRequired,
sortBy: shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
}).isRequired,
wrapping: string.isRequired,
fieldNames: arrayOf(
shape({
internalName: string.isRequired,
displayName: string.isRequired,
visible: bool.isRequired,
})
).isRequired,
fixFirstColumn: bool,
}),
hoverTime: string, hoverTime: string,
onSetHoverTime: func, onSetHoverTime: func,
colors: arrayOf( colors: arrayOf(
@ -353,6 +402,7 @@ TableGraph.propTypes = {
value: string.isRequired, value: string.isRequired,
}).isRequired }).isRequired
), ),
setDataLabels: func,
} }
export default TableGraph export default TableGraph

View File

@ -3,17 +3,23 @@ import _ from 'lodash'
export const NULL_COLUMN_INDEX = -1 export const NULL_COLUMN_INDEX = -1
export const NULL_ROW_INDEX = -1 export const NULL_ROW_INDEX = -1
export const NULL_ARRAY_INDEX = -1
export const NULL_HOVER_TIME = '0' export const NULL_HOVER_TIME = '0'
export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.ss' export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.ss'
export const TIME_FORMAT_CUSTOM = 'Custom' export const TIME_FORMAT_CUSTOM = 'Custom'
export const TIME_COLUMN_DEFAULT = {internalName: 'time', displayName: ''} export const TIME_FIELD_DEFAULT = {
internalName: 'time',
displayName: '',
visible: true,
}
export const ASCENDING = 'asc' export const ASCENDING = 'asc'
export const DESCENDING = 'desc' export const DESCENDING = 'desc'
export const FIX_FIRST_COLUMN_DEFAULT = true export const FIX_FIRST_COLUMN_DEFAULT = true
export const VERTICAL_TIME_AXIS_DEFAULT = true
export const CELL_HORIZONTAL_PADDING = 18 export const CELL_HORIZONTAL_PADDING = 18
@ -30,11 +36,11 @@ export const FORMAT_OPTIONS = [
] ]
export const DEFAULT_TABLE_OPTIONS = { export const DEFAULT_TABLE_OPTIONS = {
verticalTimeAxis: true, verticalTimeAxis: VERTICAL_TIME_AXIS_DEFAULT,
timeFormat: TIME_FORMAT_DEFAULT, timeFormat: TIME_FORMAT_DEFAULT,
sortBy: TIME_COLUMN_DEFAULT, sortBy: TIME_FIELD_DEFAULT,
wrapping: 'truncate', wrapping: 'truncate',
columnNames: [TIME_COLUMN_DEFAULT], fieldNames: [TIME_FIELD_DEFAULT],
fixFirstColumn: FIX_FIRST_COLUMN_DEFAULT, fixFirstColumn: FIX_FIRST_COLUMN_DEFAULT,
} }

View File

@ -13,8 +13,6 @@ export const THRESHOLD_TYPE_TEXT = 'text'
export const THRESHOLD_TYPE_BG = 'background' export const THRESHOLD_TYPE_BG = 'background'
export const THRESHOLD_TYPE_BASE = 'base' export const THRESHOLD_TYPE_BASE = 'base'
export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss.ss'
export const THRESHOLD_COLORS = [ export const THRESHOLD_COLORS = [
{ {
hex: '#BF3D5E', hex: '#BF3D5E',
@ -121,16 +119,6 @@ export const DEFAULT_THRESHOLDS_LIST_COLORS = [
}, },
] ]
export const DEFAULT_TABLE_COLORS = [
{
type: THRESHOLD_TYPE_BG,
hex: THRESHOLD_COLORS[18].hex,
id: THRESHOLD_TYPE_BASE,
name: THRESHOLD_COLORS[18].name,
value: 0,
},
]
export const validateThresholdsListColors = (colors, type) => { export const validateThresholdsListColors = (colors, type) => {
if (!colors || colors.length === 0) { if (!colors || colors.length === 0) {
return DEFAULT_THRESHOLDS_LIST_COLORS return DEFAULT_THRESHOLDS_LIST_COLORS

View File

@ -207,7 +207,7 @@ $graph-type--gutter: 4px;
} }
.gauge-controls--section, .gauge-controls--section,
.column-controls--section { .field-controls--section {
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -256,18 +256,34 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
flex: 1 0 0; flex: 1 0 0;
} }
.field-controls--group {
.column-controls--label { max-height: 235px;
overflow-y: scroll;
}
.field-controls--label, .field-controls--label-hidden {
@extend %gauge-controls-label-styles; @extend %gauge-controls-label-styles;
color: $g16-pearl; color: $g16-pearl;
background-color: $g4-onyx; background-color: $g4-onyx;
flex: 2 0 0; flex: 2 0 0;
> span {
padding-right: 5px;
&:hover {
cursor: pointer;
}
}
} }
.column-controls-input { .field-controls--label-hidden {
color: $g0-obsidian;
}
.field-controls-input {
flex: 1 0 0; flex: 1 0 0;
display: flex; display: flex;
align-items: center; align-items: center;
} }
/* /*
Cell Editor Overlay - Single-Stat Controls Cell Editor Overlay - Single-Stat Controls
------------------------------------------------------------------------------ ------------------------------------------------------------------------------

View File

@ -178,8 +178,10 @@ export const timeSeriesToTableGraph = raw => {
const tableData = map(sortedTimeSeries, ({time, values}) => [time, ...values]) const tableData = map(sortedTimeSeries, ({time, values}) => [time, ...values])
const data = tableData.length ? [labels, ...tableData] : [[]] const data = tableData.length ? [labels, ...tableData] : [[]]
return {
return {data} labels,
data,
}
} }
export default timeSeriesToDygraph export default timeSeriesToDygraph

View File

@ -1,62 +0,0 @@
import React from 'react'
import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
import {shallow} from 'enzyme'
const setup = (override = {}) => {
const props = {
displayName: '',
internalName: '',
onColumnRename: () => {},
...override,
}
const wrapper = shallow(<GraphOptionsCustomizableColumn {...props} />)
const instance = wrapper.instance() as GraphOptionsCustomizableColumn
return {wrapper, props, instance}
}
describe('Dashboards.Components.GraphOptionsCustomizableColumn', () => {
describe('rendering', () => {
it('displays both label div and InputClickToEdit', () => {
const {wrapper} = setup()
const label = wrapper.find('div').last()
const input = wrapper.find(InputClickToEdit)
expect(label.exists()).toBe(true)
expect(input.exists()).toBe(true)
})
describe('when there is an internalName', () => {
it('displays the value', () => {
const internalName = 'test'
const {wrapper} = setup({internalName})
const label = wrapper.find('div').last()
expect(label.exists()).toBe(true)
expect(label.children().contains(internalName)).toBe(true)
})
})
})
describe('instance methods', () => {
describe('#handleColumnRename', () => {
it('calls onColumnRename once', () => {
const onColumnRename = jest.fn()
const internalName = 'test'
const {instance} = setup({onColumnRename, internalName})
const rename = 'TEST'
instance.handleColumnRename(rename)
expect(onColumnRename).toHaveBeenCalledTimes(1)
expect(onColumnRename).toHaveBeenCalledWith({
displayName: rename,
internalName,
})
})
})
})
})

View File

@ -0,0 +1,101 @@
import {shallow} from 'enzyme'
import React from 'react'
import GraphOptionsCustomizableField from 'src/dashboards/components/GraphOptionsCustomizableField'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
const setup = (override = {}) => {
const props = {
displayName: '',
internalName: '',
onFieldUpdate: () => {},
visible: true,
...override,
}
const wrapper = shallow(<GraphOptionsCustomizableField {...props} />)
const instance = wrapper.instance() as GraphOptionsCustomizableField
return {wrapper, props, instance}
}
describe('Dashboards.Components.GraphOptionsCustomizableField', () => {
describe('rendering', () => {
it('displays both label div and InputClickToEdit', () => {
const {wrapper} = setup()
const label = wrapper.find('div').last()
const input = wrapper.find(InputClickToEdit)
const icon = wrapper.find('span')
expect(label.exists()).toBe(true)
expect(icon.exists()).toBe(true)
expect(input.exists()).toBe(true)
})
describe('when there is an internalName', () => {
it('displays the value', () => {
const internalName = 'test'
const {wrapper} = setup({internalName})
const label = wrapper.find('div').last()
const icon = wrapper.find('span')
expect(label.exists()).toBe(true)
expect(label.children().contains(internalName)).toBe(true)
expect(icon.exists()).toBe(true)
})
})
describe('when visible is false', () => {
it('displays disabled inputClickToEdit', () => {
const visible = false
const {wrapper} = setup({visible})
const input = wrapper.find(InputClickToEdit)
expect(input.prop('disabled')).toBe(!visible)
})
})
})
describe('instance methods', () => {
describe('#handleFieldUpdate', () => {
it('calls onFieldUpdate once with internalName, new name, and visible', () => {
const onFieldUpdate = jest.fn()
const internalName = 'test'
const {instance, props: {visible}} = setup({
internalName,
onFieldUpdate,
})
const rename = 'TEST'
instance.handleFieldRename(rename)
expect(onFieldUpdate).toHaveBeenCalledTimes(1)
expect(onFieldUpdate).toHaveBeenCalledWith({
displayName: rename,
internalName,
visible,
})
})
})
describe('#handleToggleVisible', () => {
it('calls onFieldUpdate once with !visible, internalName, and displayName', () => {
const onFieldUpdate = jest.fn()
const visible = true
const {instance, props: {internalName, displayName}} = setup({
onFieldUpdate,
visible,
})
instance.handleToggleVisible()
expect(onFieldUpdate).toHaveBeenCalledTimes(1)
expect(onFieldUpdate).toHaveBeenCalledWith({
displayName,
internalName,
visible: !visible,
})
})
})
})
})

View File

@ -1,34 +0,0 @@
import React from 'react'
import GraphOptionsCustomizableColumn from 'src/dashboards/components/GraphOptionsCustomizableColumn'
import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns'
import {TIME_COLUMN_DEFAULT} from 'src/shared/constants/tableGraph'
import {shallow} from 'enzyme'
const setup = (override = {}) => {
const props = {
columns: [],
onColumnRename: () => {},
...override,
}
const wrapper = shallow(<GraphOptionsCustomizeColumns {...props} />)
return {wrapper, props}
}
describe('Dashboards.Components.GraphOptionsCustomizeColumns', () => {
describe('rendering', () => {
it('displays label and all columns passed in', () => {
const columns = [TIME_COLUMN_DEFAULT]
const {wrapper} = setup({columns})
const label = wrapper.find('label')
const customizableColumns = wrapper.find(GraphOptionsCustomizableColumn)
expect(label.exists()).toBe(true)
expect(customizableColumns.exists()).toBe(true)
expect(customizableColumns.length).toBe(columns.length)
})
})
})

View File

@ -0,0 +1,33 @@
import {shallow} from 'enzyme'
import React from 'react'
import GraphOptionsCustomizableField from 'src/dashboards/components/GraphOptionsCustomizableField'
import GraphOptionsCustomizeFields from 'src/dashboards/components/GraphOptionsCustomizeFields'
import {TIME_FIELD_DEFAULT} from 'src/shared/constants/tableGraph'
const setup = (override = {}) => {
const props = {
fields: [],
onFieldUpdate: () => {},
...override,
}
const wrapper = shallow(<GraphOptionsCustomizeFields {...props} />)
return {wrapper, props}
}
describe('Dashboards.Components.GraphOptionsCustomizableField', () => {
describe('rendering', () => {
it('displays label and all fields passed in', () => {
const fields = [TIME_FIELD_DEFAULT]
const {wrapper} = setup({fields})
const label = wrapper.find('label')
const CustomizableFields = wrapper.find(GraphOptionsCustomizableField)
expect(label.exists()).toBe(true)
expect(CustomizableFields.exists()).toBe(true)
expect(CustomizableFields.length).toBe(fields.length)
})
})
})

View File

@ -1,59 +1,26 @@
import {shallow} from 'enzyme'
import React from 'react' import React from 'react'
import GraphOptionsCustomizeFields from 'src/dashboards/components/GraphOptionsCustomizeFields'
import {TableOptions} from 'src/dashboards/components/TableOptions'
import GraphOptionsCustomizeColumns from 'src/dashboards/components/GraphOptionsCustomizeColumns'
import GraphOptionsFixFirstColumn from 'src/dashboards/components/GraphOptionsFixFirstColumn' import GraphOptionsFixFirstColumn from 'src/dashboards/components/GraphOptionsFixFirstColumn'
import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy' import GraphOptionsSortBy from 'src/dashboards/components/GraphOptionsSortBy'
import GraphOptionsTextWrapping from 'src/dashboards/components/GraphOptionsTextWrapping'
import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis' import GraphOptionsTimeAxis from 'src/dashboards/components/GraphOptionsTimeAxis'
import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat' import GraphOptionsTimeFormat from 'src/dashboards/components/GraphOptionsTimeFormat'
import {TableOptions} from 'src/dashboards/components/TableOptions'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import ThresholdsList from 'src/shared/components/ThresholdsList' import ThresholdsList from 'src/shared/components/ThresholdsList'
import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle' import ThresholdsListTypeToggle from 'src/shared/components/ThresholdsListTypeToggle'
import {shallow} from 'enzyme'
const queryConfigs = [
{
fields: [
{
alias: 'boom',
value: 'test',
},
{
alias: 'again',
value: 'again',
},
],
measurement: 'dev',
},
{
fields: [
{
alias: 'boom',
value: 'test',
},
{
alias: 'again',
value: 'again',
},
],
measurement: 'prod',
},
]
const defaultProps = { const defaultProps = {
dataLabels: [],
handleUpdateTableOptions: () => {}, handleUpdateTableOptions: () => {},
onResetFocus: () => {}, onResetFocus: () => {},
queryConfigs,
tableOptions: { tableOptions: {
columnNames: [], columnNames: [],
fieldNames: [],
fixFirstColumn: true, fixFirstColumn: true,
sortBy: {internalName: '', displayName: ''}, sortBy: {displayName: '', internalName: '', visible: true},
timeFormat: '', timeFormat: '',
verticalTimeAxis: true, verticalTimeAxis: true,
wrapping: '',
}, },
} }
@ -70,89 +37,31 @@ const setup = (override = {}) => {
describe('Dashboards.Components.TableOptions', () => { describe('Dashboards.Components.TableOptions', () => {
describe('getters', () => { describe('getters', () => {
describe('computedColumnNames', () => { describe('computedColumnNames', () => {})
it('returns the correct column names', () => {
const instance = new TableOptions(defaultProps)
const expected = [
{
displayName: '',
internalName: 'time',
},
{
displayName: '',
internalName: 'dev.boom',
},
{
displayName: '',
internalName: 'dev.again',
},
{
displayName: '',
internalName: 'prod.boom',
},
{
displayName: '',
internalName: 'prod.again',
},
]
expect(instance.computedColumnNames).toEqual(expected)
})
})
}) })
describe('rendering', () => { describe('rendering', () => {
it('should render all components', () => { it('should render all components', () => {
const qc = [ const {wrapper} = setup()
{
fields: [
{
alias: 'boom',
value: 'test',
},
],
measurement: 'dev',
},
]
const expectedSortOptions = [
{
key: 'time',
text: 'time',
},
{
key: 'dev.boom',
text: 'dev.boom',
},
]
const {wrapper} = setup({queryConfigs: qc})
const fancyScrollbar = wrapper.find(FancyScrollbar) const fancyScrollbar = wrapper.find(FancyScrollbar)
const graphOptionsTimeFormat = wrapper.find(GraphOptionsTimeFormat) const graphOptionsTimeFormat = wrapper.find(GraphOptionsTimeFormat)
const graphOptionsTimeAxis = wrapper.find(GraphOptionsTimeAxis) const graphOptionsTimeAxis = wrapper.find(GraphOptionsTimeAxis)
const graphOptionsSortBy = wrapper.find(GraphOptionsSortBy) const graphOptionsSortBy = wrapper.find(GraphOptionsSortBy)
const graphOptionsTextWrapping = wrapper.find(GraphOptionsTextWrapping)
const graphOptionsFixFirstColumn = wrapper.find( const graphOptionsFixFirstColumn = wrapper.find(
GraphOptionsFixFirstColumn GraphOptionsFixFirstColumn
) )
const graphOptionsCustomizeColumns = wrapper.find( const graphOptionsCustomizeFields = wrapper.find(
GraphOptionsCustomizeColumns GraphOptionsCustomizeFields
) )
const thresholdsList = wrapper.find(ThresholdsList) const thresholdsList = wrapper.find(ThresholdsList)
const thresholdsListTypeToggle = wrapper.find(ThresholdsListTypeToggle) const thresholdsListTypeToggle = wrapper.find(ThresholdsListTypeToggle)
expect(graphOptionsSortBy.props().sortByOptions).toEqual(
expectedSortOptions
)
expect(fancyScrollbar.exists()).toBe(true) expect(fancyScrollbar.exists()).toBe(true)
expect(graphOptionsTimeFormat.exists()).toBe(true) expect(graphOptionsTimeFormat.exists()).toBe(true)
expect(graphOptionsTimeAxis.exists()).toBe(true) expect(graphOptionsTimeAxis.exists()).toBe(true)
expect(graphOptionsSortBy.exists()).toBe(true) expect(graphOptionsSortBy.exists()).toBe(true)
expect(graphOptionsTextWrapping.exists()).toBe(true)
expect(graphOptionsFixFirstColumn.exists()).toBe(true) expect(graphOptionsFixFirstColumn.exists()).toBe(true)
expect(graphOptionsCustomizeColumns.exists()).toBe(true) expect(graphOptionsCustomizeFields.exists()).toBe(true)
expect(thresholdsList.exists()).toBe(true) expect(thresholdsList.exists()).toBe(true)
expect(thresholdsListTypeToggle.exists()).toBe(true) expect(thresholdsListTypeToggle.exists()).toBe(true)
}) })