diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9c62017c..4d9d899f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,23 @@ ## v1.3.6.0 [unreleased] ### Bug Fixes +1. [#1798](https://github.com/influxdata/chronograf/pull/1798): Fix domain not updating in visualizations when changing time range manually +1. [#1799](https://github.com/influxdata/chronograf/pull/1799): Prevent console error spam from Dygraph.synchronize when a dashboard has only one graph +1. [#1813](https://github.com/influxdata/chronograf/pull/1813): Guarantee UUID for each Alert Table key to prevent dropping items when keys overlap + ### Features 1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu +1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis bounds +1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis label ### UI Improvements 1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written +1. [#1800](https://github.com/influxdata/chronograf/pull/1796): Embiggen text area for line protocol manual entry in Data Explorer's Write Data overlay +1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Fix bar graphs overlapping +1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Add series names hashing so that graph colors should stay the same for the same series across charts +1. [#1800](https://github.com/influxdata/chronograf/pull/1800): Embiggen text area for line protocol manual entry in Data Explorer's Write Data overlay +1. [#1812](https://github.com/influxdata/chronograf/pull/1812): Improve error message when request for Status Page News Feed fails -## v1.3.5.0 [2017-07-25] +## v1.3.5.0 [2017-07-27] ### Bug Fixes 1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu 1. [#1752](https://github.com/influxdata/chronograf/pull/1752): Clarify BoltPath server flag help text by making example the default path diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index 912d125618..6d5fbc6b74 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -183,14 +183,9 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { axes := make(map[string]*Axis, len(c.Axes)) for a, r := range c.Axes { - // need to explicitly allocate a new array because r.Bounds is - // over-written and the resulting slices from previous iterations will - // point to later iteration's data. It is _not_ enough to simply re-slice - // r.Bounds - axis := [2]int64{} - copy(axis[:], r.Bounds[:2]) axes[a] = &Axis{ - Bounds: axis[:], + Bounds: r.Bounds, + Label: r.Label, } } @@ -269,9 +264,16 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { axes := make(map[string]chronograf.Axis, len(c.Axes)) for a, r := range c.Axes { - axis := chronograf.Axis{} - copy(axis.Bounds[:], r.Bounds[:2]) - axes[a] = axis + if r.Bounds != nil { + axes[a] = chronograf.Axis{ + Bounds: r.Bounds, + Label: r.Label, + } + } else { + axes[a] = chronograf.Axis{ + Bounds: []string{}, + } + } } cells[i] = chronograf.DashboardCell{ diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index eef73feb1b..d8eb0e222f 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -118,7 +118,9 @@ func (m *DashboardCell) GetAxes() map[string]*Axis { } type Axis struct { - Bounds []int64 `protobuf:"varint,1,rep,name=bounds" json:"bounds,omitempty"` + LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"` + Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"` + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` } func (m *Axis) Reset() { *m = Axis{} } @@ -313,65 +315,66 @@ func init() { func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } var fileDescriptorInternal = []byte{ - // 952 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4, - 0x13, 0x56, 0xc7, 0x76, 0x12, 0x57, 0x66, 0xe7, 0xf7, 0x53, 0x6b, 0xc5, 0x9a, 0x45, 0x42, 0xc1, - 0x02, 0x29, 0x20, 0x31, 0xa0, 0x5d, 0x21, 0x21, 0x6e, 0x99, 0x09, 0x5a, 0x85, 0x99, 0x5d, 0x86, - 0xce, 0xcc, 0x70, 0x42, 0xab, 0x4e, 0x52, 0x99, 0x58, 0xeb, 0xc4, 0xa6, 0x6d, 0x4f, 0xe2, 0xb7, - 0xe0, 0x09, 0x90, 0x90, 0x38, 0x71, 0xe0, 0xc0, 0x0b, 0xf0, 0x10, 0xbc, 0x10, 0xaa, 0xee, 0xf6, - 0x9f, 0xb0, 0xb3, 0x68, 0x4f, 0xdc, 0xfa, 0xab, 0xea, 0x7c, 0xe5, 0xfe, 0xea, 0xab, 0x52, 0xe0, - 0x38, 0xda, 0xe6, 0xa8, 0xb6, 0x32, 0x3e, 0x49, 0x55, 0x92, 0x27, 0xbc, 0x5f, 0xe1, 0xf0, 0xf7, - 0x0e, 0x74, 0x67, 0x49, 0xa1, 0x16, 0xc8, 0x8f, 0xa1, 0x33, 0x9d, 0x04, 0x6c, 0xc8, 0x46, 0x8e, - 0xe8, 0x4c, 0x27, 0x9c, 0x83, 0xfb, 0x42, 0x6e, 0x30, 0xe8, 0x0c, 0xd9, 0xc8, 0x17, 0xfa, 0x4c, - 0xb1, 0xab, 0x32, 0xc5, 0xc0, 0x31, 0x31, 0x3a, 0xf3, 0xc7, 0xd0, 0xbf, 0xce, 0x88, 0x6d, 0x83, - 0x81, 0xab, 0xe3, 0x35, 0xa6, 0xdc, 0xa5, 0xcc, 0xb2, 0x5d, 0xa2, 0x96, 0x81, 0x67, 0x72, 0x15, - 0xe6, 0xff, 0x07, 0xe7, 0x5a, 0x5c, 0x04, 0x5d, 0x1d, 0xa6, 0x23, 0x0f, 0xa0, 0x37, 0xc1, 0x95, - 0x2c, 0xe2, 0x3c, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x2a, 0x48, 0x3c, 0x57, 0x18, 0xe3, 0xad, 0x92, - 0xab, 0xa0, 0x6f, 0x78, 0x2a, 0xcc, 0x4f, 0x80, 0x4f, 0xb7, 0x19, 0x2e, 0x0a, 0x85, 0xb3, 0x57, - 0x51, 0x7a, 0x83, 0x2a, 0x5a, 0x95, 0x81, 0xaf, 0x09, 0xee, 0xc9, 0x50, 0x95, 0xe7, 0x98, 0x4b, - 0xaa, 0x0d, 0x9a, 0xaa, 0x82, 0x3c, 0x84, 0xa3, 0xd9, 0x5a, 0x2a, 0x5c, 0xce, 0x70, 0xa1, 0x30, - 0x0f, 0x06, 0x3a, 0x7d, 0x10, 0x0b, 0x7f, 0x62, 0xe0, 0x4f, 0x64, 0xb6, 0x9e, 0x27, 0x52, 0x2d, - 0xdf, 0x4a, 0xb3, 0x4f, 0xc1, 0x5b, 0x60, 0x1c, 0x67, 0x81, 0x33, 0x74, 0x46, 0x83, 0x27, 0x8f, - 0x4e, 0xea, 0x66, 0xd4, 0x3c, 0x67, 0x18, 0xc7, 0xc2, 0xdc, 0xe2, 0x9f, 0x83, 0x9f, 0xe3, 0x26, - 0x8d, 0x65, 0x8e, 0x59, 0xe0, 0xea, 0x9f, 0xf0, 0xe6, 0x27, 0x57, 0x36, 0x25, 0x9a, 0x4b, 0xe1, - 0x6f, 0x1d, 0x78, 0x70, 0x40, 0xc5, 0x8f, 0x80, 0xed, 0xf5, 0x57, 0x79, 0x82, 0xed, 0x09, 0x95, - 0xfa, 0x8b, 0x3c, 0xc1, 0x4a, 0x42, 0x3b, 0xdd, 0x3f, 0x4f, 0xb0, 0x1d, 0xa1, 0xb5, 0xee, 0x9a, - 0x27, 0xd8, 0x9a, 0x7f, 0x0c, 0xbd, 0x1f, 0x0b, 0x54, 0x11, 0x66, 0x81, 0xa7, 0x2b, 0xff, 0xaf, - 0xa9, 0xfc, 0x5d, 0x81, 0xaa, 0x14, 0x55, 0x9e, 0x5e, 0xaa, 0x3b, 0x6e, 0xda, 0xa7, 0xcf, 0x14, - 0xcb, 0xc9, 0x1d, 0x3d, 0x13, 0xa3, 0xb3, 0x55, 0xc8, 0xf4, 0x8c, 0x14, 0xfa, 0x02, 0x5c, 0xb9, - 0xc7, 0x2c, 0xf0, 0x35, 0xff, 0x07, 0x6f, 0x10, 0xe3, 0x64, 0xbc, 0xc7, 0xec, 0xeb, 0x6d, 0xae, - 0x4a, 0xa1, 0xaf, 0x3f, 0x7e, 0x06, 0x7e, 0x1d, 0x22, 0xe7, 0xbc, 0xc2, 0x52, 0x3f, 0xd0, 0x17, - 0x74, 0xe4, 0x1f, 0x82, 0x77, 0x27, 0xe3, 0xc2, 0x08, 0x3f, 0x78, 0x72, 0xdc, 0xd0, 0x8e, 0xf7, - 0x51, 0x26, 0x4c, 0xf2, 0xab, 0xce, 0x97, 0x2c, 0x7c, 0x1f, 0x5c, 0x0a, 0xf1, 0x77, 0xa0, 0x3b, - 0x4f, 0x8a, 0xed, 0x32, 0x0b, 0xd8, 0xd0, 0x19, 0x39, 0xc2, 0xa2, 0xf0, 0x4f, 0x46, 0x56, 0x33, - 0xd2, 0xb6, 0xda, 0x6b, 0x3e, 0xfe, 0x5d, 0xe8, 0x93, 0xec, 0x2f, 0xef, 0xa4, 0xb2, 0x2d, 0xee, - 0x11, 0xbe, 0x91, 0x8a, 0x7f, 0x06, 0x5d, 0x5d, 0xe4, 0x9e, 0x36, 0x57, 0x74, 0x37, 0x94, 0x17, - 0xf6, 0x5a, 0x2d, 0x96, 0xdb, 0x12, 0xeb, 0x21, 0x78, 0xb1, 0x9c, 0x63, 0x6c, 0x67, 0xc5, 0x00, - 0x32, 0x10, 0xa9, 0x5e, 0x6a, 0xad, 0xef, 0x65, 0x36, 0xbd, 0x31, 0xb7, 0xc2, 0x6b, 0x78, 0x70, - 0x50, 0xb1, 0xae, 0xc4, 0x0e, 0x2b, 0x35, 0x82, 0xf9, 0x56, 0x20, 0x1a, 0xb3, 0x0c, 0x63, 0x5c, - 0xe4, 0xb8, 0xd4, 0x16, 0xe9, 0x8b, 0x1a, 0x87, 0xbf, 0xb0, 0x86, 0x57, 0xd7, 0xa3, 0x41, 0x5a, - 0x24, 0x9b, 0x8d, 0xdc, 0x2e, 0x2d, 0x75, 0x05, 0x49, 0xb7, 0xe5, 0xdc, 0x52, 0x77, 0x96, 0x73, - 0xc2, 0x2a, 0xb5, 0x4b, 0xa3, 0xa3, 0x52, 0x3e, 0x84, 0xc1, 0x06, 0x65, 0x56, 0x28, 0xdc, 0xe0, - 0x36, 0xb7, 0x12, 0xb4, 0x43, 0xfc, 0x11, 0xf4, 0x72, 0x79, 0xfb, 0x92, 0xda, 0x6c, 0xb4, 0xe8, - 0xe6, 0xf2, 0xf6, 0x1c, 0x4b, 0xfe, 0x1e, 0xf8, 0xab, 0x08, 0xe3, 0xa5, 0x4e, 0x19, 0xf3, 0xf5, - 0x75, 0xe0, 0x1c, 0xcb, 0xf0, 0x57, 0x06, 0xdd, 0x19, 0xaa, 0x3b, 0x54, 0x6f, 0x35, 0x99, 0xed, - 0xcd, 0xe5, 0xfc, 0xcb, 0xe6, 0x72, 0xef, 0xdf, 0x5c, 0x5e, 0xb3, 0xb9, 0x1e, 0x82, 0x37, 0x53, - 0x8b, 0xe9, 0x44, 0x7f, 0x91, 0x23, 0x0c, 0x20, 0x8f, 0x8d, 0x17, 0x79, 0x74, 0x87, 0x76, 0x9d, - 0x59, 0x14, 0xfe, 0xcc, 0xa0, 0x7b, 0x21, 0xcb, 0xa4, 0xc8, 0x5f, 0x73, 0xd8, 0x10, 0x06, 0xe3, - 0x34, 0x8d, 0xa3, 0x85, 0xcc, 0xa3, 0x64, 0x6b, 0xbf, 0xb6, 0x1d, 0xa2, 0x1b, 0xcf, 0x5b, 0xda, - 0x99, 0xef, 0x6e, 0x87, 0x68, 0x18, 0xce, 0xf4, 0xc2, 0x31, 0xdb, 0xa3, 0x35, 0x0c, 0x66, 0xcf, - 0xe8, 0x24, 0x3d, 0x70, 0x5c, 0xe4, 0xc9, 0x2a, 0x4e, 0x76, 0xfa, 0x25, 0x7d, 0x51, 0xe3, 0xf0, - 0x2f, 0x06, 0xee, 0x7f, 0xb5, 0x48, 0x8e, 0x80, 0x45, 0xb6, 0x91, 0x2c, 0xaa, 0xd7, 0x4a, 0xaf, - 0xb5, 0x56, 0x02, 0xe8, 0x95, 0x4a, 0x6e, 0x6f, 0x31, 0x0b, 0xfa, 0x7a, 0x56, 0x2b, 0xa8, 0x33, - 0x7a, 0x46, 0xcc, 0x3e, 0xf1, 0x45, 0x05, 0x6b, 0xcf, 0x43, 0xe3, 0xf9, 0xf0, 0x0f, 0x06, 0x5e, - 0xed, 0xdc, 0xb3, 0x43, 0xe7, 0x9e, 0x35, 0xce, 0x9d, 0x9c, 0x56, 0xce, 0x9d, 0x9c, 0x12, 0x16, - 0x97, 0x95, 0x73, 0xc5, 0x25, 0xa9, 0xf6, 0x4c, 0x25, 0x45, 0x7a, 0x5a, 0x1a, 0x79, 0x7d, 0x51, - 0x63, 0x6a, 0xf7, 0xf7, 0x6b, 0x54, 0xf6, 0xcd, 0xbe, 0xb0, 0x88, 0xcc, 0x71, 0xa1, 0xa7, 0xda, - 0xbc, 0xd2, 0x00, 0xfe, 0x11, 0x78, 0x82, 0x5e, 0xa1, 0x9f, 0x7a, 0x20, 0x90, 0x0e, 0x0b, 0x93, - 0x0d, 0x9f, 0xda, 0x6b, 0xc4, 0x72, 0x9d, 0xa6, 0xa8, 0xac, 0xa7, 0x0d, 0xd0, 0xdc, 0xc9, 0x0e, - 0xcd, 0x3a, 0x72, 0x84, 0x01, 0xe1, 0x0f, 0xe0, 0x8f, 0x63, 0x54, 0xb9, 0x28, 0xe2, 0xd7, 0x97, - 0x18, 0x07, 0xf7, 0x9b, 0xd9, 0xb7, 0x2f, 0xaa, 0x49, 0xa0, 0x73, 0xe3, 0x5f, 0xe7, 0x1f, 0xfe, - 0x3d, 0x97, 0xa9, 0x9c, 0x4e, 0x74, 0x63, 0x1d, 0x61, 0x51, 0xf8, 0x09, 0xb8, 0x34, 0x27, 0x2d, - 0x66, 0xf7, 0x4d, 0x33, 0x36, 0xef, 0xea, 0x7f, 0x1c, 0x4f, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, - 0x94, 0xd8, 0xce, 0x85, 0x83, 0x08, 0x00, 0x00, + // 974 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xdf, 0x6e, 0xe3, 0xc4, + 0x17, 0xd6, 0xc4, 0x76, 0x12, 0x9f, 0x76, 0xfb, 0xfb, 0x69, 0xb4, 0x62, 0xcd, 0x72, 0x13, 0x2c, + 0x90, 0x02, 0x12, 0x05, 0xed, 0x0a, 0x09, 0x71, 0x97, 0x36, 0x68, 0x55, 0xda, 0x5d, 0xca, 0xa4, + 0x2d, 0xdc, 0xa0, 0xd5, 0xc4, 0x39, 0x4d, 0xac, 0x75, 0x62, 0x33, 0xb6, 0x9b, 0xf8, 0x2d, 0x78, + 0x02, 0x24, 0x24, 0xae, 0xb8, 0xe0, 0x82, 0x17, 0xe0, 0x21, 0x78, 0x21, 0x74, 0x66, 0xc6, 0x7f, + 0xc2, 0x76, 0xd1, 0x5e, 0x71, 0x37, 0xdf, 0x39, 0xe3, 0x6f, 0x66, 0xbe, 0xf3, 0x9d, 0x23, 0xc3, + 0x51, 0xbc, 0x29, 0x50, 0x6d, 0x64, 0x72, 0x9c, 0xa9, 0xb4, 0x48, 0xf9, 0xb0, 0xc6, 0xe1, 0xef, + 0x3d, 0xe8, 0xcf, 0xd2, 0x52, 0x45, 0xc8, 0x8f, 0xa0, 0x77, 0x36, 0x0d, 0xd8, 0x88, 0x8d, 0x1d, + 0xd1, 0x3b, 0x9b, 0x72, 0x0e, 0xee, 0x0b, 0xb9, 0xc6, 0xa0, 0x37, 0x62, 0x63, 0x5f, 0xe8, 0x35, + 0xc5, 0xae, 0xaa, 0x0c, 0x03, 0xc7, 0xc4, 0x68, 0xcd, 0x1f, 0xc3, 0xf0, 0x3a, 0x27, 0xb6, 0x35, + 0x06, 0xae, 0x8e, 0x37, 0x98, 0x72, 0x97, 0x32, 0xcf, 0xb7, 0xa9, 0x5a, 0x04, 0x9e, 0xc9, 0xd5, + 0x98, 0xff, 0x1f, 0x9c, 0x6b, 0x71, 0x11, 0xf4, 0x75, 0x98, 0x96, 0x3c, 0x80, 0xc1, 0x14, 0x6f, + 0x65, 0x99, 0x14, 0xc1, 0x60, 0xc4, 0xc6, 0x43, 0x51, 0x43, 0xe2, 0xb9, 0xc2, 0x04, 0x97, 0x4a, + 0xde, 0x06, 0x43, 0xc3, 0x53, 0x63, 0x7e, 0x0c, 0xfc, 0x6c, 0x93, 0x63, 0x54, 0x2a, 0x9c, 0xbd, + 0x8a, 0xb3, 0x1b, 0x54, 0xf1, 0x6d, 0x15, 0xf8, 0x9a, 0xe0, 0x9e, 0x0c, 0x9d, 0xf2, 0x1c, 0x0b, + 0x49, 0x67, 0x83, 0xa6, 0xaa, 0x21, 0x0f, 0xe1, 0x70, 0xb6, 0x92, 0x0a, 0x17, 0x33, 0x8c, 0x14, + 0x16, 0xc1, 0x81, 0x4e, 0xef, 0xc5, 0xc2, 0x9f, 0x18, 0xf8, 0x53, 0x99, 0xaf, 0xe6, 0xa9, 0x54, + 0x8b, 0xb7, 0xd2, 0xec, 0x13, 0xf0, 0x22, 0x4c, 0x92, 0x3c, 0x70, 0x46, 0xce, 0xf8, 0xe0, 0xc9, + 0xa3, 0xe3, 0xa6, 0x18, 0x0d, 0xcf, 0x29, 0x26, 0x89, 0x30, 0xbb, 0xf8, 0x67, 0xe0, 0x17, 0xb8, + 0xce, 0x12, 0x59, 0x60, 0x1e, 0xb8, 0xfa, 0x13, 0xde, 0x7e, 0x72, 0x65, 0x53, 0xa2, 0xdd, 0x14, + 0xfe, 0xd6, 0x83, 0x07, 0x7b, 0x54, 0xfc, 0x10, 0xd8, 0x4e, 0xdf, 0xca, 0x13, 0x6c, 0x47, 0xa8, + 0xd2, 0x37, 0xf2, 0x04, 0xab, 0x08, 0x6d, 0x75, 0xfd, 0x3c, 0xc1, 0xb6, 0x84, 0x56, 0xba, 0x6a, + 0x9e, 0x60, 0x2b, 0xfe, 0x11, 0x0c, 0x7e, 0x2c, 0x51, 0xc5, 0x98, 0x07, 0x9e, 0x3e, 0xf9, 0x7f, + 0xed, 0xc9, 0xdf, 0x96, 0xa8, 0x2a, 0x51, 0xe7, 0xe9, 0xa5, 0xba, 0xe2, 0xa6, 0x7c, 0x7a, 0x4d, + 0xb1, 0x82, 0xdc, 0x31, 0x30, 0x31, 0x5a, 0x5b, 0x85, 0x4c, 0xcd, 0x48, 0xa1, 0xcf, 0xc1, 0x95, + 0x3b, 0xcc, 0x03, 0x5f, 0xf3, 0xbf, 0xff, 0x06, 0x31, 0x8e, 0x27, 0x3b, 0xcc, 0xbf, 0xda, 0x14, + 0xaa, 0x12, 0x7a, 0xfb, 0xe3, 0x67, 0xe0, 0x37, 0x21, 0x72, 0xce, 0x2b, 0xac, 0xf4, 0x03, 0x7d, + 0x41, 0x4b, 0xfe, 0x01, 0x78, 0x77, 0x32, 0x29, 0x8d, 0xf0, 0x07, 0x4f, 0x8e, 0x5a, 0xda, 0xc9, + 0x2e, 0xce, 0x85, 0x49, 0x7e, 0xd9, 0xfb, 0x82, 0x85, 0xdf, 0x83, 0x4b, 0x21, 0xaa, 0x75, 0x82, + 0x4b, 0x19, 0x55, 0x27, 0x69, 0xb9, 0x59, 0xe4, 0x01, 0x1b, 0x39, 0x63, 0x47, 0xec, 0xc5, 0xf8, + 0x3b, 0xd0, 0x9f, 0x9b, 0x6c, 0x6f, 0xe4, 0x8c, 0x7d, 0x61, 0x11, 0x7f, 0x08, 0x5e, 0x22, 0xe7, + 0x98, 0xd8, 0x36, 0x30, 0x20, 0xfc, 0x93, 0x91, 0x49, 0x4d, 0x51, 0x3a, 0xc6, 0x30, 0xcf, 0x7e, + 0x17, 0x86, 0x54, 0xb0, 0x97, 0x77, 0x52, 0x59, 0x73, 0x0c, 0x08, 0xdf, 0x48, 0xc5, 0x3f, 0x85, + 0xbe, 0xbe, 0xde, 0x3d, 0x06, 0xa9, 0xe9, 0x6e, 0x28, 0x2f, 0xec, 0xb6, 0x46, 0x66, 0xb7, 0x23, + 0x73, 0x73, 0x25, 0xaf, 0x73, 0x25, 0xb2, 0x1e, 0xd5, 0xab, 0xd2, 0x55, 0xba, 0x97, 0xd9, 0x54, + 0xd5, 0xec, 0x0a, 0xaf, 0xe1, 0xc1, 0xde, 0x89, 0xcd, 0x49, 0x6c, 0xff, 0xa4, 0x56, 0x6a, 0xdf, + 0x4a, 0x4b, 0x0d, 0x9a, 0x63, 0x82, 0x51, 0x81, 0x0b, 0xad, 0xca, 0x50, 0x34, 0x38, 0xfc, 0x85, + 0xb5, 0xbc, 0xfa, 0x3c, 0x6a, 0xc1, 0x28, 0x5d, 0xaf, 0xe5, 0x66, 0x61, 0xa9, 0x6b, 0x48, 0xba, + 0x2d, 0xe6, 0x96, 0xba, 0xb7, 0x98, 0x13, 0x56, 0x99, 0xd5, 0xb9, 0xa7, 0x32, 0x3e, 0x82, 0x83, + 0x35, 0xca, 0xbc, 0x54, 0xb8, 0xc6, 0x4d, 0x61, 0x25, 0xe8, 0x86, 0xf8, 0x23, 0x18, 0x14, 0x72, + 0xf9, 0x92, 0x0c, 0x62, 0xb4, 0xe8, 0x17, 0x72, 0x79, 0x8e, 0x15, 0x7f, 0x0f, 0xfc, 0xdb, 0x18, + 0x93, 0x85, 0x4e, 0x19, 0xdb, 0x0e, 0x75, 0xe0, 0x1c, 0xab, 0xf0, 0x57, 0x06, 0xfd, 0x19, 0xaa, + 0x3b, 0x54, 0x6f, 0xd5, 0xd3, 0xdd, 0x99, 0xe7, 0xfc, 0xcb, 0xcc, 0x73, 0xef, 0x9f, 0x79, 0x5e, + 0x3b, 0xf3, 0x1e, 0x82, 0x37, 0x53, 0xd1, 0xd9, 0x54, 0xdf, 0xc8, 0x11, 0x06, 0x90, 0xf3, 0x26, + 0x51, 0x11, 0xdf, 0xa1, 0x1d, 0x84, 0x16, 0x85, 0x3f, 0x33, 0xe8, 0x5f, 0xc8, 0x2a, 0x2d, 0x8b, + 0xd7, 0x1c, 0x36, 0x82, 0x83, 0x49, 0x96, 0x25, 0x71, 0x24, 0x8b, 0x38, 0xdd, 0xd8, 0xdb, 0x76, + 0x43, 0xb4, 0xe3, 0x79, 0x47, 0x3b, 0x73, 0xef, 0x6e, 0x88, 0xda, 0xe8, 0x54, 0x8f, 0x2a, 0x33, + 0x77, 0x3a, 0x6d, 0x64, 0x26, 0x94, 0x4e, 0xd2, 0x03, 0x27, 0x65, 0x91, 0xde, 0x26, 0xe9, 0x56, + 0xbf, 0x64, 0x28, 0x1a, 0x1c, 0xfe, 0xc5, 0xc0, 0xfd, 0xaf, 0x46, 0xd0, 0x21, 0xb0, 0xd8, 0x16, + 0x92, 0xc5, 0xcd, 0x40, 0x1a, 0x74, 0x06, 0x52, 0x00, 0x83, 0x4a, 0xc9, 0xcd, 0x12, 0xf3, 0x60, + 0xa8, 0xfb, 0xbb, 0x86, 0x3a, 0xa3, 0x7b, 0xc4, 0x4c, 0x22, 0x5f, 0xd4, 0xb0, 0xf1, 0x3c, 0xb4, + 0x9e, 0x0f, 0xff, 0x60, 0xe0, 0x35, 0xce, 0x3d, 0xdd, 0x77, 0xee, 0x69, 0xeb, 0xdc, 0xe9, 0x49, + 0xed, 0xdc, 0xe9, 0x09, 0x61, 0x71, 0x59, 0x3b, 0x57, 0x5c, 0x92, 0x6a, 0xcf, 0x54, 0x5a, 0x66, + 0x27, 0x95, 0x91, 0xd7, 0x17, 0x0d, 0xa6, 0x72, 0x7f, 0xb7, 0x42, 0x65, 0xdf, 0xec, 0x0b, 0x8b, + 0xc8, 0x1c, 0x17, 0xba, 0xab, 0xcd, 0x2b, 0x0d, 0xe0, 0x1f, 0x82, 0x27, 0xe8, 0x15, 0xfa, 0xa9, + 0x7b, 0x02, 0xe9, 0xb0, 0x30, 0xd9, 0xf0, 0xa9, 0xdd, 0x46, 0x2c, 0xd7, 0x59, 0x86, 0xca, 0x7a, + 0xda, 0x00, 0xcd, 0x9d, 0x6e, 0xd1, 0x8c, 0x23, 0x47, 0x18, 0x10, 0xfe, 0x00, 0xfe, 0x24, 0x41, + 0x55, 0x88, 0x32, 0x79, 0x7d, 0x88, 0x71, 0x70, 0xbf, 0x9e, 0x7d, 0xf3, 0xa2, 0xee, 0x04, 0x5a, + 0xb7, 0xfe, 0x75, 0xfe, 0xe1, 0xdf, 0x73, 0x99, 0xc9, 0xb3, 0xa9, 0x2e, 0xac, 0x23, 0x2c, 0x0a, + 0x3f, 0x06, 0x97, 0xfa, 0xa4, 0xc3, 0xec, 0xbe, 0xa9, 0xc7, 0xe6, 0x7d, 0xfd, 0xaf, 0xf2, 0xf4, + 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x79, 0x79, 0x87, 0x37, 0xbd, 0x08, 0x00, 0x00, } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index 5154a216a8..c2c208693d 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -35,7 +35,9 @@ message DashboardCell { } message Axis { - repeated int64 bounds = 1; // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively + repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively + repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds. + string label = 3; // label is a description of this axis } message Template { diff --git a/bolt/internal/internal_test.go b/bolt/internal/internal_test.go index cbfa2d459a..2a17f67378 100644 --- a/bolt/internal/internal_test.go +++ b/bolt/internal/internal_test.go @@ -160,7 +160,8 @@ func Test_MarshalDashboard(t *testing.T) { }, Axes: map[string]chronograf.Axis{ "y": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "3", "1-7", "foo"}, + Label: "foo", }, }, Type: "line", @@ -179,3 +180,149 @@ func Test_MarshalDashboard(t *testing.T) { t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(dashboard, actual)) } } + +func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) { + dashboard := chronograf.Dashboard{ + ID: 1, + Cells: []chronograf.DashboardCell{ + { + ID: "9b5367de-c552-4322-a9e8-7f384cbd235c", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "Super awesome query", + Queries: []chronograf.DashboardQuery{ + { + Command: "select * from cpu", + Label: "CPU Utilization", + Range: &chronograf.Range{ + Upper: int64(100), + }, + }, + }, + Axes: map[string]chronograf.Axis{ + "y": chronograf.Axis{ + LegacyBounds: [2]int64{0, 5}, + }, + }, + Type: "line", + }, + }, + Templates: []chronograf.Template{}, + Name: "Dashboard", + } + + expected := chronograf.Dashboard{ + ID: 1, + Cells: []chronograf.DashboardCell{ + { + ID: "9b5367de-c552-4322-a9e8-7f384cbd235c", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "Super awesome query", + Queries: []chronograf.DashboardQuery{ + { + Command: "select * from cpu", + Label: "CPU Utilization", + Range: &chronograf.Range{ + Upper: int64(100), + }, + }, + }, + Axes: map[string]chronograf.Axis{ + "y": chronograf.Axis{ + Bounds: []string{}, + }, + }, + Type: "line", + }, + }, + Templates: []chronograf.Template{}, + Name: "Dashboard", + } + + var actual chronograf.Dashboard + if buf, err := internal.MarshalDashboard(dashboard); err != nil { + t.Fatal("Error marshaling dashboard: err", err) + } else if err := internal.UnmarshalDashboard(buf, &actual); err != nil { + t.Fatal("Error unmarshaling dashboard: err:", err) + } else if !cmp.Equal(expected, actual) { + t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual)) + } +} + +func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) { + dashboard := chronograf.Dashboard{ + ID: 1, + Cells: []chronograf.DashboardCell{ + { + ID: "9b5367de-c552-4322-a9e8-7f384cbd235c", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "Super awesome query", + Queries: []chronograf.DashboardQuery{ + { + Command: "select * from cpu", + Label: "CPU Utilization", + Range: &chronograf.Range{ + Upper: int64(100), + }, + }, + }, + Axes: map[string]chronograf.Axis{ + "y": chronograf.Axis{ + LegacyBounds: [2]int64{}, + }, + }, + Type: "line", + }, + }, + Templates: []chronograf.Template{}, + Name: "Dashboard", + } + + expected := chronograf.Dashboard{ + ID: 1, + Cells: []chronograf.DashboardCell{ + { + ID: "9b5367de-c552-4322-a9e8-7f384cbd235c", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "Super awesome query", + Queries: []chronograf.DashboardQuery{ + { + Command: "select * from cpu", + Label: "CPU Utilization", + Range: &chronograf.Range{ + Upper: int64(100), + }, + }, + }, + Axes: map[string]chronograf.Axis{ + "y": chronograf.Axis{ + Bounds: []string{}, + }, + }, + Type: "line", + }, + }, + Templates: []chronograf.Template{}, + Name: "Dashboard", + } + + var actual chronograf.Dashboard + if buf, err := internal.MarshalDashboard(dashboard); err != nil { + t.Fatal("Error marshaling dashboard: err", err) + } else if err := internal.UnmarshalDashboard(buf, &actual); err != nil { + t.Fatal("Error unmarshaling dashboard: err:", err) + } else if !cmp.Equal(expected, actual) { + t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual)) + } +} diff --git a/chronograf.go b/chronograf.go index 4bf273910c..319065befc 100644 --- a/chronograf.go +++ b/chronograf.go @@ -569,7 +569,9 @@ type Dashboard struct { // Axis represents the visible extents of a visualization type Axis struct { - Bounds [2]int64 `json:"bounds"` // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively + Bounds []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a cell + LegacyBounds [2]int64 `json:"-"` // legacy bounds are for testing a migration from an earlier version of axis + Label string `json:"label"` // label is a description of this Axis } // DashboardCell holds visual and query information for a cell diff --git a/mocks/dashboards.go b/mocks/dashboards.go new file mode 100644 index 0000000000..a038f468ba --- /dev/null +++ b/mocks/dashboards.go @@ -0,0 +1,37 @@ +package mocks + +import ( + "context" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.DashboardsStore = &DashboardsStore{} + +type DashboardsStore struct { + AddF func(ctx context.Context, newDashboard chronograf.Dashboard) (chronograf.Dashboard, error) + AllF func(ctx context.Context) ([]chronograf.Dashboard, error) + DeleteF func(ctx context.Context, target chronograf.Dashboard) error + GetF func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) + UpdateF func(ctx context.Context, target chronograf.Dashboard) error +} + +func (d *DashboardsStore) Add(ctx context.Context, newDashboard chronograf.Dashboard) (chronograf.Dashboard, error) { + return d.AddF(ctx, newDashboard) +} + +func (d *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, error) { + return d.AllF(ctx) +} + +func (d *DashboardsStore) Delete(ctx context.Context, target chronograf.Dashboard) error { + return d.DeleteF(ctx, target) +} + +func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) { + return d.GetF(ctx, id) +} + +func (d *DashboardsStore) Update(ctx context.Context, target chronograf.Dashboard) error { + return d.UpdateF(ctx, target) +} diff --git a/mocks/logger.go b/mocks/logger.go index 46c7bae9f6..f2926e09a3 100644 --- a/mocks/logger.go +++ b/mocks/logger.go @@ -3,6 +3,7 @@ package mocks import ( "fmt" "io" + "testing" "github.com/influxdata/chronograf" ) @@ -72,3 +73,11 @@ func (tl *TestLogger) stringifyArg(arg interface{}) []byte { return []byte("UNKNOWN") } } + +// Dump dumps out logs into a given testing.T's logs +func (tl *TestLogger) Dump(t *testing.T) { + t.Log("== Dumping Test Logs ==") + for _, msg := range tl.Messages { + t.Logf("lvl: %s, msg: %s", msg.Level, msg.Body) + } +} diff --git a/server/cells.go b/server/cells.go index f60437c910..29c0bd57a2 100644 --- a/server/cells.go +++ b/server/cells.go @@ -30,11 +30,34 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC base := "/chronograf/v1/dashboards" cells := make([]dashboardCellResponse, len(dcells)) for i, cell := range dcells { - if len(cell.Queries) == 0 { - cell.Queries = make([]chronograf.DashboardQuery, 0) + newCell := chronograf.DashboardCell{} + + newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries)) + copy(newCell.Queries, cell.Queries) + + // ensure x, y, and y2 axes always returned + labels := []string{"x", "y", "y2"} + newCell.Axes = make(map[string]chronograf.Axis, len(labels)) + + newCell.X = cell.X + newCell.Y = cell.Y + newCell.W = cell.W + newCell.H = cell.H + newCell.Name = cell.Name + newCell.ID = cell.ID + + for _, lbl := range labels { + if axis, found := cell.Axes[lbl]; !found { + newCell.Axes[lbl] = chronograf.Axis{ + Bounds: []string{}, + } + } else { + newCell.Axes[lbl] = axis + } } + cells[i] = dashboardCellResponse{ - DashboardCell: cell, + DashboardCell: newCell, Links: dashboardCellLinks{ Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID), }, diff --git a/server/cells_test.go b/server/cells_test.go index be694b3dff..5ca5dce5e1 100644 --- a/server/cells_test.go +++ b/server/cells_test.go @@ -1,9 +1,18 @@ package server_test import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" "testing" + "github.com/bouk/httprouter" + "github.com/google/go-cmp/cmp" "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/mocks" "github.com/influxdata/chronograf/server" ) @@ -20,13 +29,13 @@ func Test_Cells_CorrectAxis(t *testing.T) { &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "100"}, }, "y": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "100"}, }, "y2": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "100"}, }, }, }, @@ -37,10 +46,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "axis of evil": chronograf.Axis{ - Bounds: [2]int64{666, 666}, + Bounds: []string{"666", "666"}, }, "axis of awesome": chronograf.Axis{ - Bounds: [2]int64{1337, 31337}, + Bounds: []string{"1337", "31337"}, }, }, }, @@ -58,3 +67,137 @@ func Test_Cells_CorrectAxis(t *testing.T) { }) } } + +func Test_Service_DashboardCells(t *testing.T) { + cellsTests := []struct { + name string + reqURL *url.URL + ctxParams map[string]string + mockResponse []chronograf.DashboardCell + expected []chronograf.DashboardCell + expectedCode int + }{ + { + "happy path", + &url.URL{ + Path: "/chronograf/v1/dashboards/1/cells", + }, + map[string]string{ + "id": "1", + }, + []chronograf.DashboardCell{}, + []chronograf.DashboardCell{}, + http.StatusOK, + }, + { + "cell axes should always be \"x\", \"y\", and \"y2\"", + &url.URL{ + Path: "/chronograf/v1/dashboards/1/cells", + }, + map[string]string{ + "id": "1", + }, + []chronograf.DashboardCell{ + { + ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "CPU", + Queries: []chronograf.DashboardQuery{}, + Axes: map[string]chronograf.Axis{}, + }, + }, + []chronograf.DashboardCell{ + { + ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "CPU", + Queries: []chronograf.DashboardQuery{}, + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Bounds: []string{}, + }, + "y": chronograf.Axis{ + Bounds: []string{}, + }, + "y2": chronograf.Axis{ + Bounds: []string{}, + }, + }, + }, + }, + http.StatusOK, + }, + } + + for _, test := range cellsTests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + // setup context with params + ctx := context.Background() + params := httprouter.Params{} + for k, v := range test.ctxParams { + params = append(params, httprouter.Param{k, v}) + } + ctx = httprouter.WithParams(ctx, params) + + // setup response recorder and request + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", test.reqURL.RequestURI(), strings.NewReader("")).WithContext(ctx) + + // setup mock DashboardCells store and logger + tlog := &mocks.TestLogger{} + svc := &server.Service{ + DashboardsStore: &mocks.DashboardsStore{ + GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) { + return chronograf.Dashboard{ + ID: chronograf.DashboardID(1), + Cells: test.mockResponse, + Templates: []chronograf.Template{}, + Name: "empty dashboard", + }, nil + }, + }, + Logger: tlog, + } + + // invoke DashboardCell handler + svc.DashboardCells(rr, req) + + // setup frame to decode response into + respFrame := []struct { + chronograf.DashboardCell + Links json.RawMessage `json:"links"` // ignore links + }{} + + // decode response + resp := rr.Result() + + if resp.StatusCode != test.expectedCode { + tlog.Dump(t) + t.Fatalf("%q - Status codes do not match. Want %d (%s), Got %d (%s)", test.name, test.expectedCode, http.StatusText(test.expectedCode), resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + if err := json.NewDecoder(resp.Body).Decode(&respFrame); err != nil { + t.Fatalf("%q - Error unmarshaling response body: err: %s", test.name, err) + } + + // extract actual + actual := []chronograf.DashboardCell{} + for _, rsp := range respFrame { + actual = append(actual, rsp.DashboardCell) + } + + // compare actual and expected + if !cmp.Equal(actual, test.expected) { + t.Fatalf("%q - Dashboard Cells do not match: diff: %s", test.name, cmp.Diff(actual, test.expected)) + } + }) + } +} diff --git a/server/dashboards.go b/server/dashboards.go index ee9bb8a602..62e6aac1c5 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -28,20 +28,19 @@ type getDashboardsResponse struct { func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse { base := "/chronograf/v1/dashboards" - DashboardDefaults(&d) - AddQueryConfigs(&d) - cells := newCellResponses(d.ID, d.Cells) - templates := newTemplateResponses(d.ID, d.Templates) + dd := AddQueryConfigs(DashboardDefaults(d)) + cells := newCellResponses(dd.ID, dd.Cells) + templates := newTemplateResponses(dd.ID, dd.Templates) return &dashboardResponse{ - ID: d.ID, - Name: d.Name, + ID: dd.ID, + Name: dd.Name, Cells: cells, Templates: templates, Links: dashboardLinks{ - Self: fmt.Sprintf("%s/%d", base, d.ID), - Cells: fmt.Sprintf("%s/%d/cells", base, d.ID), - Templates: fmt.Sprintf("%s/%d/templates", base, d.ID), + Self: fmt.Sprintf("%s/%d", base, dd.ID), + Cells: fmt.Sprintf("%s/%d/cells", base, dd.ID), + Templates: fmt.Sprintf("%s/%d/templates", base, dd.ID), }, } } @@ -229,24 +228,36 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error { return err } } - DashboardDefaults(d) + (*d) = DashboardDefaults(*d) return nil } // DashboardDefaults updates the dashboard with the default values // if none are specified -func DashboardDefaults(d *chronograf.Dashboard) { +func DashboardDefaults(d chronograf.Dashboard) (newDash chronograf.Dashboard) { + newDash.ID = d.ID + newDash.Templates = d.Templates + newDash.Name = d.Name + newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells)) + for i, c := range d.Cells { CorrectWidthHeight(&c) - d.Cells[i] = c + newDash.Cells[i] = c } + return } // AddQueryConfigs updates all the celsl in the dashboard to have query config // objects corresponding to their influxql queries. -func AddQueryConfigs(d *chronograf.Dashboard) { +func AddQueryConfigs(d chronograf.Dashboard) (newDash chronograf.Dashboard) { + newDash.ID = d.ID + newDash.Templates = d.Templates + newDash.Name = d.Name + newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells)) + for i, c := range d.Cells { AddQueryConfig(&c) - d.Cells[i] = c + newDash.Cells[i] = c } + return } diff --git a/server/dashboards_test.go b/server/dashboards_test.go index b5116913c5..ff740ba001 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -128,7 +128,7 @@ func TestDashboardDefaults(t *testing.T) { }, } for _, tt := range tests { - if DashboardDefaults(&tt.d); !reflect.DeepEqual(tt.d, tt.want) { + if actual := DashboardDefaults(tt.d); !reflect.DeepEqual(actual, tt.want) { t.Errorf("%q. DashboardDefaults() = %v, want %v", tt.name, tt.d, tt.want) } } @@ -222,10 +222,11 @@ func Test_newDashboardResponse(t *testing.T) { }, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "100"}, }, "y": chronograf.Axis{ - Bounds: [2]int64{2, 95}, + Bounds: []string{"2", "95"}, + Label: "foo", }, }, }, @@ -268,10 +269,14 @@ func Test_newDashboardResponse(t *testing.T) { }, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ - Bounds: [2]int64{0, 100}, + Bounds: []string{"0", "100"}, }, "y": chronograf.Axis{ - Bounds: [2]int64{2, 95}, + Bounds: []string{"2", "95"}, + Label: "foo", + }, + "y2": chronograf.Axis{ + Bounds: []string{}, }, }, }, @@ -284,6 +289,17 @@ func Test_newDashboardResponse(t *testing.T) { ID: "b", W: 4, H: 4, + Axes: map[string]chronograf.Axis{ + "x": chronograf.Axis{ + Bounds: []string{}, + }, + "y": chronograf.Axis{ + Bounds: []string{}, + }, + "y2": chronograf.Axis{ + Bounds: []string{}, + }, + }, Queries: []chronograf.DashboardQuery{ { Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", diff --git a/ui/spec/dashboards/reducers/uiSpec.js b/ui/spec/dashboards/reducers/uiSpec.js index 537e851eb2..62ef3e54dd 100644 --- a/ui/spec/dashboards/reducers/uiSpec.js +++ b/ui/spec/dashboards/reducers/uiSpec.js @@ -60,6 +60,7 @@ const c1 = { w: 4, h: 4, id: 1, + i: 'im-a-cell-id-index', isEditing: false, name: 'Gigawatts', } @@ -71,18 +72,6 @@ const editingCell = { } const cells = [c1] -const tempVar = { - ...d1.templates[0], - id: '1', - type: 'measurement', - label: 'test query', - tempVar: '$HOSTS', - query: { - db: 'db1', - text: 'SHOW TAGS WHERE HUNTER = "coo"', - }, - values: ['h1', 'h2', 'h3'], -} describe('DataExplorer.Reducers.UI', () => { it('can load the dashboards', () => { diff --git a/ui/spec/shared/parsing/getRangeForDygraphSpec.js b/ui/spec/shared/parsing/getRangeForDygraphSpec.js index d467a7c79f..1c3c7d80ef 100644 --- a/ui/spec/shared/parsing/getRangeForDygraphSpec.js +++ b/ui/spec/shared/parsing/getRangeForDygraphSpec.js @@ -17,10 +17,18 @@ describe('getRangeForDygraphSpec', () => { it('does not get range when a range is provided', () => { const timeSeries = [[date, min], [date, max], [date, mid]] - const providedRange = [0, 4] + const providedRange = ['0', '4'] const actual = getRange(timeSeries, providedRange) - expect(actual).to.deep.equal(providedRange) + expect(actual).to.deep.equal([0, 4]) + }) + + it('does not use the user submitted range if they are equal', () => { + const timeSeries = [[date, min], [date, max], [date, mid]] + const providedRange = ['0', '0'] + const actual = getRange(timeSeries, providedRange) + + expect(actual).to.deep.equal([min, max]) }) it('gets the range for multiple timeSeries', () => { diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.js index f5dfce8932..fba3e21095 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.js @@ -1,7 +1,9 @@ import React, {Component, PropTypes} from 'react' + import _ from 'lodash' import classnames from 'classnames' import {Link} from 'react-router' +import uuid from 'node-uuid' import FancyScrollbar from 'shared/components/FancyScrollbar' @@ -130,10 +132,7 @@ class AlertsTable extends Component { > {alerts.map(({name, level, time, host, value}) => { return ( -
+
{ + const min = _.get(axes, ['y', 'bounds', '0'], '') + const max = _.get(axes, ['y', 'bounds', '1'], '') + const label = _.get(axes, ['y', 'label'], '') + + return ( +
+
Y Axis Controls
+
+
+ + +
+
+ + +
+
+ + +
+

+ Values left blank will be set automatically +

+ {/*
+ + +
+
+ + +
+
+ +
    +
  • K/M/B
  • +
  • K/M/G
  • +
+
+
+ +
    +
  • Linear
  • +
  • Logarithmic
  • +
+
*/} +
+
+ ) +} + +const {arrayOf, func, shape, string} = PropTypes + +AxesOptions.propTypes = { + onSetRange: func.isRequired, + onSetLabel: func.isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), + label: string, + }), + }).isRequired, +} + +export default AxesOptions diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 791ab72523..57ce0d3492 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -7,12 +7,15 @@ import ResizeContainer from 'shared/components/ResizeContainer' import QueryMaker from 'src/data_explorer/components/QueryMaker' import Visualization from 'src/data_explorer/components/Visualization' import OverlayControls from 'src/dashboards/components/OverlayControls' +import DisplayOptions from 'src/dashboards/components/DisplayOptions' + import * as queryModifiers from 'src/utils/queryTransitions' import defaultQueryConfig from 'src/utils/defaultQueryConfig' import buildInfluxQLQuery from 'utils/influxql' import {getQueryConfig} from 'shared/apis' +import {buildYLabel} from 'shared/presenters' import {removeUnselectedTemplateValues} from 'src/dashboards/constants' import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' @@ -22,17 +25,17 @@ class CellEditorOverlay extends Component { super(props) this.queryStateReducer = ::this.queryStateReducer - this.handleAddQuery = ::this.handleAddQuery this.handleDeleteQuery = ::this.handleDeleteQuery - this.handleSaveCell = ::this.handleSaveCell - this.handleSelectGraphType = ::this.handleSelectGraphType + this.handleClickDisplayOptionsTab = ::this.handleClickDisplayOptionsTab this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex this.handleEditRawText = ::this.handleEditRawText + this.handleSetYAxisBounds = ::this.handleSetYAxisBounds + this.handleSetLabel = ::this.handleSetLabel - const {cell: {name, type, queries}} = props + const {cell: {name, type, queries, axes}} = props const queriesWorkingDraft = _.cloneDeep( queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})) @@ -43,9 +46,26 @@ class CellEditorOverlay extends Component { cellWorkingType: type, queriesWorkingDraft, activeQueryIndex: 0, + isDisplayOptionsTabActive: false, + axes: this.setDefaultLabels(axes, queries), } } + setDefaultLabels(axes, queries) { + if (!queries.length) { + return axes + } + + if (axes.y.label) { + return axes + } + + const q = queries[0].queryConfig + const label = buildYLabel(q) + + return {...axes, y: {...axes.y, label}} + } + componentWillReceiveProps(nextProps) { const {status, queryID} = this.props.queryStatus const nextStatus = nextProps.queryStatus @@ -73,6 +93,24 @@ class CellEditorOverlay extends Component { } } + handleSetYAxisBounds(e) { + const {min, max} = e.target.form + const {axes} = this.state + + this.setState({ + axes: {...axes, y: {...axes.y, bounds: [min.value, max.value]}}, + }) + e.preventDefault() + } + + handleSetLabel(e) { + const {label} = e.target.form + const {axes} = this.state + + this.setState({axes: {...axes, y: {...axes.y, label: label.value}}}) + e.preventDefault() + } + handleAddQuery(options) { const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options) const nextQueries = this.state.queriesWorkingDraft.concat(newQuery) @@ -87,31 +125,44 @@ class CellEditorOverlay extends Component { } handleSaveCell() { - const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state + const { + queriesWorkingDraft, + cellWorkingType: type, + cellWorkingName: name, + axes, + } = this.state + const {cell} = this.props - const newCell = _.cloneDeep(cell) - newCell.name = cellWorkingName - newCell.type = cellWorkingType - newCell.queries = queriesWorkingDraft.map(q => { + const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} const query = q.rawText || buildInfluxQLQuery(timeRange, q) - const label = q.rawText ? '' : `${q.measurement}.${q.fields[0].field}` return { queryConfig: q, query, - label, } }) - this.props.onSave(newCell) + this.props.onSave({ + ...cell, + name, + type, + queries, + axes, + }) } handleSelectGraphType(graphType) { this.setState({cellWorkingType: graphType}) } + handleClickDisplayOptionsTab(isDisplayOptionsTabActive) { + return () => { + this.setState({isDisplayOptionsTabActive}) + } + } + handleSetActiveQueryIndex(activeQueryIndex) { this.setState({activeQueryIndex}) } @@ -146,7 +197,9 @@ class CellEditorOverlay extends Component { activeQueryIndex, cellWorkingName, cellWorkingType, + isDisplayOptionsTabActive, queriesWorkingDraft, + axes, } = this.state const queryActions = { @@ -177,27 +230,36 @@ class CellEditorOverlay extends Component { cellType={cellWorkingType} cellName={cellWorkingName} editQueryStatus={editQueryStatus} + axes={axes} views={[]} />
- + {isDisplayOptionsTabActive + ? + : }
@@ -232,6 +294,7 @@ CellEditorOverlay.propTypes = { queryID: string, status: shape({}), }).isRequired, + dashboardID: string.isRequired, } export default CellEditorOverlay diff --git a/ui/src/dashboards/components/DisplayOptions.js b/ui/src/dashboards/components/DisplayOptions.js new file mode 100644 index 0000000000..693ad4da78 --- /dev/null +++ b/ui/src/dashboards/components/DisplayOptions.js @@ -0,0 +1,31 @@ +import React, {PropTypes} from 'react' + +import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector' +import AxesOptions from 'src/dashboards/components/AxesOptions' + +const DisplayOptions = ({ + selectedGraphType, + onSelectGraphType, + onSetLabel, + onSetRange, + axes, +}) => +
+ + +
+ +const {func, shape, string} = PropTypes + +DisplayOptions.propTypes = { + selectedGraphType: string.isRequired, + onSelectGraphType: func.isRequired, + onSetRange: func.isRequired, + onSetLabel: func.isRequired, + axes: shape({}).isRequired, +} + +export default DisplayOptions diff --git a/ui/src/dashboards/components/GraphTypeSelector.js b/ui/src/dashboards/components/GraphTypeSelector.js new file mode 100644 index 0000000000..ee23f444a2 --- /dev/null +++ b/ui/src/dashboards/components/GraphTypeSelector.js @@ -0,0 +1,35 @@ +import React, {PropTypes} from 'react' +import classnames from 'classnames' + +import {graphTypes} from 'src/dashboards/graphics/graph' + +const GraphTypeSelector = ({selectedGraphType, onSelectGraphType}) => +
+
Visualization Type
+
+ {graphTypes.map(graphType => +
+
onSelectGraphType(graphType.type)}> + {graphType.graphic} +

+ {graphType.menuOption} +

+
+
+ )} +
+
+ +const {func, string} = PropTypes + +GraphTypeSelector.propTypes = { + selectedGraphType: string.isRequired, + onSelectGraphType: func.isRequired, +} + +export default GraphTypeSelector diff --git a/ui/src/dashboards/components/OverlayControls.js b/ui/src/dashboards/components/OverlayControls.js index a13a29f72e..ba4ac6fda4 100644 --- a/ui/src/dashboards/components/OverlayControls.js +++ b/ui/src/dashboards/components/OverlayControls.js @@ -3,51 +3,51 @@ import classnames from 'classnames' import ConfirmButtons from 'shared/components/ConfirmButtons' -import graphTypes from 'hson!shared/data/graphTypes.hson' - -const OverlayControls = props => { - const { - onCancel, - onSave, - selectedGraphType, - onSelectGraphType, - isSavable, - } = props - return ( -
-

Cell Editor

-
-

Visualization Type:

-
    - {graphTypes.map(graphType => -
  • onSelectGraphType(graphType.type)} - > - {graphType.menuOption} -
  • - )} -
- -
+const OverlayControls = ({ + onCancel, + onSave, + isDisplayOptionsTabActive, + onClickDisplayOptions, + isSavable, +}) => +
+

Cell Editor

+
    +
  • + Queries +
  • +
  • + Display Options +
  • +
+
+
- ) -} +
-const {func, string, bool} = PropTypes +const {func, bool} = PropTypes OverlayControls.propTypes = { onCancel: func.isRequired, onSave: func.isRequired, - selectedGraphType: string.isRequired, - onSelectGraphType: func.isRequired, + isDisplayOptionsTabActive: bool.isRequired, + onClickDisplayOptions: func.isRequired, isSavable: bool, } diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 82941d6801..b4b801694c 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -209,7 +209,11 @@ class DashboardPage extends Component { const dygraphs = [...this.state.dygraphs, dygraph] const {dashboards, params} = this.props const dashboard = dashboards.find(d => d.id === +params.dashboardID) - if (dashboard && dygraphs.length === dashboard.cells.length) { + if ( + dashboard && + dygraphs.length === dashboard.cells.length && + dashboard.cells.length > 1 + ) { Dygraph.synchronize(dygraphs, { selection: true, zoom: false, @@ -248,7 +252,7 @@ class DashboardPage extends Component { inPresentationMode, handleChooseAutoRefresh, handleClickPresentationButton, - params: {sourceID}, + params: {sourceID, dashboardID}, } = this.props const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant' @@ -322,6 +326,7 @@ class DashboardPage extends Component { {selectedCell ? + + + + + + + + +
+ ), + }, + { + type: 'line-stacked', + menuOption: 'Stacked', + graphic: ( +
+ + + + + + + + +
+ ), + }, + { + type: 'line-stepplot', + menuOption: 'Step-Plot', + graphic: ( +
+ + + + + + +
+ ), + }, + { + type: 'single-stat', + menuOption: 'SingleStat', + graphic: ( +
+ + + + + + +
+ ), + }, + { + type: 'line-plus-single-stat', + menuOption: 'Line + Stat', + graphic: ( +
+ + + + + + + + + + +
+ ), + }, + { + type: 'bar', + menuOption: 'Bar', + graphic: ( +
+ + + + + + + + + + + + + + +
+ ), + }, +] diff --git a/ui/src/data_explorer/components/VisView.js b/ui/src/data_explorer/components/VisView.js index 057d605d34..9e717c6f14 100644 --- a/ui/src/data_explorer/components/VisView.js +++ b/ui/src/data_explorer/components/VisView.js @@ -1,13 +1,10 @@ import React, {PropTypes} from 'react' import Table from './Table' -import AutoRefresh from 'shared/components/AutoRefresh' -import LineGraph from 'shared/components/LineGraph' -import SingleStat from 'shared/components/SingleStat' -const RefreshingLineGraph = AutoRefresh(LineGraph) -const RefreshingSingleStat = AutoRefresh(SingleStat) +import RefreshingGraph from 'shared/components/RefreshingGraph' const VisView = ({ + axes, view, queries, cellType, @@ -16,7 +13,6 @@ const VisView = ({ heightPixels, editQueryStatus, activeQueryIndex, - isInDataExplorer, }) => { const activeQuery = queries[activeQueryIndex] const defaultQuery = queries[0] @@ -40,40 +36,23 @@ const VisView = ({ ) } - if (cellType === 'single-stat') { - return ( - - ) - } - - const displayOptions = { - stepPlot: cellType === 'line-stepplot', - stackedGraph: cellType === 'line-stacked', - } - return ( - ) } -const {arrayOf, bool, func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes VisView.propTypes = { view: string.isRequired, + axes: shape(), queries: arrayOf(shape()).isRequired, cellType: string, templates: arrayOf(shape()), @@ -81,7 +60,6 @@ VisView.propTypes = { heightPixels: number, editQueryStatus: func.isRequired, activeQueryIndex: number, - isInDataExplorer: bool, } export default VisView diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index 452c4f5a44..b33b99a7b3 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -26,6 +26,11 @@ const Visualization = React.createClass({ heightPixels: number, editQueryStatus: func.isRequired, views: arrayOf(string).isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), + }), + }), }, contextTypes: { @@ -48,6 +53,7 @@ const Visualization = React.createClass({ getDefaultProps() { return { cellName: '', + cellType: '', } }, @@ -76,6 +82,7 @@ const Visualization = React.createClass({ render() { const { + axes, views, height, cellType, @@ -93,11 +100,13 @@ const Visualization = React.createClass({ const {view} = this.state const statements = queryConfigs.map(query => { - const text = query.rawText || buildInfluxQLQuery(timeRange, query) - return {text, id: query.id} + const text = + query.rawText || buildInfluxQLQuery(query.range || timeRange, query) + return {text, id: query.id, queryConfig: query} }) + const queries = statements.filter(s => s.text !== null).map(s => { - return {host: [proxy], text: s.text, id: s.id} + return {host: [proxy], text: s.text, id: s.id, queryConfig: s.queryConfig} }) return ( @@ -116,6 +125,7 @@ const Visualization = React.createClass({ > - {uploadContent diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index e0c3265e12..2574c19512 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {removeUnselectedTemplateValues} from 'src/dashboards/constants' const { + array, arrayOf, bool, element, @@ -43,6 +44,12 @@ const AutoRefresh = ComposedComponent => { text: string, }).isRequired ).isRequired, + axes: shape({ + bounds: shape({ + y: array, + y2: array, + }), + }), editQueryStatus: func, }, diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index cfdb06968c..e90fa28fd1 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -9,6 +9,13 @@ import getRange from 'shared/parsing/getRangeForDygraph' import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers' import DygraphLegend from 'src/shared/components/DygraphLegend' +import {buildYLabel} from 'shared/presenters' + +const hasherino = (str, len) => + str + .split('') + .map(char => char.charCodeAt(0)) + .reduce((hash, code) => hash + code, 0) % len export default class Dygraph extends Component { constructor(props) { @@ -35,12 +42,14 @@ export default class Dygraph extends Component { this.handleHideLegend = ::this.handleHideLegend this.handleToggleFilter = ::this.handleToggleFilter this.visibility = ::this.visibility + this.getLabel = ::this.getLabel } static defaultProps = { containerStyle: {}, isGraphFilled: true, overrideLineColors: null, + dygraphRef: () => {}, } getTimeSeries() { @@ -50,11 +59,23 @@ export default class Dygraph extends Component { return timeSeries.length ? timeSeries : [[0]] } + getLabel(axis) { + const {axes, queries} = this.props + const label = _.get(axes, [axis, 'label'], '') + const queryConfig = _.get(queries, ['0', 'queryConfig'], false) + + if (label || !queryConfig) { + return label + } + + return buildYLabel(queryConfig) + } + componentDidMount() { const timeSeries = this.getTimeSeries() // dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'}; const { - ranges, + axes, dygraphSeries, ruleValues, overrideLineColors, @@ -65,18 +86,29 @@ export default class Dygraph extends Component { const graphRef = this.graphRef const legendRef = this.legendRef - let finalLineColors = overrideLineColors + const finalLineColors = [...(overrideLineColors || LINE_COLORS)] - if (finalLineColors === null) { - finalLineColors = LINE_COLORS + const hashColorDygraphSeries = {} + const {length} = finalLineColors + + for (const seriesName in dygraphSeries) { + const series = dygraphSeries[seriesName] + const hashIndex = hasherino(seriesName, length) + const color = finalLineColors[hashIndex] + hashColorDygraphSeries[seriesName] = {...series, color} } + const yAxis = _.get(axes, ['y', 'bounds'], [null, null]) + const y2Axis = _.get(axes, ['y2', 'bounds'], undefined) + const defaultOptions = { - plugins: [ - new Dygraphs.Plugins.Crosshair({ - direction: 'vertical', - }), - ], + plugins: isBarGraph + ? [] + : [ + new Dygraphs.Plugins.Crosshair({ + direction: 'vertical', + }), + ], labelsSeparateLines: false, labelsKMB: true, rightGap: 0, @@ -85,22 +117,22 @@ export default class Dygraph extends Component { fillGraph: isGraphFilled, axisLineWidth: 2, gridLineWidth: 1, - highlightCircleSize: 3, + highlightCircleSize: isBarGraph ? 0 : 3, animatedZooms: true, hideOverlayOnMouseOut: false, colors: finalLineColors, - series: dygraphSeries, + series: hashColorDygraphSeries, axes: { y: { - valueRange: getRange(timeSeries, ranges.y, ruleValues), + valueRange: getRange(timeSeries, yAxis, ruleValues), }, y2: { - valueRange: getRange(timeSeries, ranges.y2), + valueRange: getRange(timeSeries, y2Axis), }, }, highlightSeriesOpts: { strokeWidth: 2, - highlightCircleSize: 5, + highlightCircleSize: isBarGraph ? 0 : 5, }, legendFormatter: legend => { if (!legend.x) { @@ -235,11 +267,12 @@ export default class Dygraph extends Component { componentDidUpdate() { const { labels, - ranges, + axes, options, dygraphSeries, ruleValues, isBarGraph, + overrideLineColors, } = this.props const dygraph = this.dygraph @@ -249,22 +282,39 @@ export default class Dygraph extends Component { ) } + const y = _.get(axes, ['y', 'bounds'], [null, null]) + const y2 = _.get(axes, ['y2', 'bounds'], undefined) const timeSeries = this.getTimeSeries() + const ylabel = this.getLabel('y') + const finalLineColors = [...(overrideLineColors || LINE_COLORS)] + + const hashColorDygraphSeries = {} + const {length} = finalLineColors + + for (const seriesName in dygraphSeries) { + const series = dygraphSeries[seriesName] + const hashIndex = hasherino(seriesName, length) + const color = finalLineColors[hashIndex] + hashColorDygraphSeries[seriesName] = {...series, color} + } + const updateOptions = { labels, file: timeSeries, + ylabel, axes: { y: { - valueRange: getRange(timeSeries, ranges.y, ruleValues), + valueRange: getRange(timeSeries, y, ruleValues), }, y2: { - valueRange: getRange(timeSeries, ranges.y2), + valueRange: getRange(timeSeries, y2), }, }, stepPlot: options.stepPlot, stackedGraph: options.stackedGraph, underlayCallback: options.underlayCallback, - series: dygraphSeries, + colors: finalLineColors, + series: hashColorDygraphSeries, plotter: isBarGraph ? multiColumnBarPlotter : null, visibility: this.visibility(), } @@ -350,6 +400,7 @@ export default class Dygraph extends Component {
{ this.graphRef = r + this.props.dygraphRef(r) }} style={this.props.containerStyle} className="dygraph-child-container" @@ -359,13 +410,18 @@ export default class Dygraph extends Component { } } -const {array, arrayOf, func, number, bool, shape, string} = PropTypes +const {array, arrayOf, bool, func, shape, string} = PropTypes Dygraph.propTypes = { - ranges: shape({ - y: arrayOf(number), - y2: arrayOf(number), + axes: shape({ + y: shape({ + bounds: array, + }), + y2: shape({ + bounds: array, + }), }), + queries: arrayOf(shape), timeSeries: array.isRequired, labels: array.isRequired, options: shape({}), @@ -384,4 +440,5 @@ Dygraph.propTypes = { }), synchronizer: func, setResolution: func, + dygraphRef: func, } diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 9a751a4281..61f1fc11ad 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -151,7 +151,7 @@ class LayoutRenderer extends Component { } = this.props return cells.map(cell => { - const {type, h} = cell + const {type, h, axes} = cell return (
@@ -175,6 +175,7 @@ class LayoutRenderer extends Component { type={type} queries={this.standardizeQueries(cell, source)} cellHeight={h} + axes={axes} />}
diff --git a/ui/src/shared/components/LineGraph.js b/ui/src/shared/components/LineGraph.js index f3a3808f03..3bcd691150 100644 --- a/ui/src/shared/components/LineGraph.js +++ b/ui/src/shared/components/LineGraph.js @@ -2,7 +2,6 @@ import React, {PropTypes} from 'react' import Dygraph from 'shared/components/Dygraph' import classnames from 'classnames' import shallowCompare from 'react-addons-shallow-compare' -import _ from 'lodash' import timeSeriesToDygraph from 'utils/timeSeriesToDygraph' import lastValues from 'shared/parsing/lastValues' @@ -15,9 +14,15 @@ export default React.createClass({ displayName: 'LineGraph', propTypes: { data: arrayOf(shape({}).isRequired).isRequired, - ranges: shape({ - y: arrayOf(number), - y2: arrayOf(number), + axes: shape({ + y: shape({ + bounds: array, + label: string, + }), + y2: shape({ + bounds: array, + label: string, + }), }), title: string, isFetchingInitially: bool, @@ -81,15 +86,15 @@ export default React.createClass({ render() { const { data, - ranges, + axes, isFetchingInitially, isRefreshing, isGraphFilled, isBarGraph, overrideLineColors, title, - underlayCallback, queries, + underlayCallback, showSingleStat, displayOptions, ruleValues, @@ -120,8 +125,6 @@ export default React.createClass({ axisLabelWidth: 60, drawAxesAtZero: true, underlayCallback, - ylabel: _.get(queries, ['0', 'label'], ''), - y2label: _.get(queries, ['1', 'label'], ''), ...displayOptions, } @@ -151,26 +154,25 @@ export default React.createClass({ roundedValue = Math.round(+lastValue * precision) / precision } + const lineColors = showSingleStat + ? singleStatLineColors + : overrideLineColors + return ( -
+
{isRefreshing ? this.renderSpinner() : null} @@ -202,25 +224,4 @@ export default React.createClass({
) }, - - getRanges() { - const {queries} = this.props - if (!queries) { - return {} - } - - const ranges = {} - const q0 = queries[0] - const q1 = queries[1] - - if (q0 && q0.range) { - ranges.y = [q0.range.lower, q0.range.upper] - } - - if (q1 && q1.range) { - ranges.y2 = [q1.range.lower, q1.range.upper] - } - - return ranges - }, }) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 65d5382dab..56e43857ff 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -15,6 +15,7 @@ const RefreshingGraph = ({ type, queries, cellHeight, + axes, }) => { if (type === 'single-stat') { return ( @@ -42,6 +43,7 @@ const RefreshingGraph = ({ isBarGraph={type === 'bar'} displayOptions={displayOptions} synchronizer={synchronizer} + axes={axes} /> ) } @@ -57,7 +59,8 @@ RefreshingGraph.propTypes = { synchronizer: func, type: string.isRequired, queries: arrayOf(shape()).isRequired, - cellHeight: number.isRequired, + cellHeight: number, + axes: shape(), } export default RefreshingGraph diff --git a/ui/src/shared/data/graphTypes.hson b/ui/src/shared/data/graphTypes.hson index 30a3565ab5..31724eb722 100644 --- a/ui/src/shared/data/graphTypes.hson +++ b/ui/src/shared/data/graphTypes.hson @@ -1,8 +1,8 @@ [ - {type: "line", menuOption: "Line"}, - {type: "line-stacked", menuOption: "Stacked"}, - {type: "line-stepplot", menuOption: "Step-Plot"}, - {type: "single-stat", menuOption: "SingleStat"}, - {type: "line-plus-single-stat", menuOption: "Line + Stat"}, - {type: "bar", menuOption: "Bar"}, + {type: "line", menuOption: "Line", graphic: "Wogglez"}, + {type: "line-stacked", menuOption: "Stacked", graphic: "Rogglez"}, + {type: "line-stepplot", menuOption: "Step-Plot", graphic: "Fogglez"}, + {type: "single-stat", menuOption: "SingleStat", graphic: "Bogglez"}, + {type: "line-plus-single-stat", menuOption: "Line + Stat", graphic: "Togglez"}, + {type: "bar", menuOption: "Bar", graphic: "Zogglez"}, ] diff --git a/ui/src/shared/graphs/helpers.js b/ui/src/shared/graphs/helpers.js index a0dc843722..33786c8b5e 100644 --- a/ui/src/shared/graphs/helpers.js +++ b/ui/src/shared/graphs/helpers.js @@ -26,7 +26,7 @@ export const darkenColor = colorStr => { return `rgb(${color.r},${color.g},${color.b})` } -// Bar Graph code below is from http://dygraphs.com/tests/plotters.html +// Bar Graph code below is adapted from http://dygraphs.com/tests/plotters.html export const multiColumnBarPlotter = e => { // We need to handle all the series simultaneously. if (e.seriesIndex !== 0) { @@ -51,24 +51,34 @@ export const multiColumnBarPlotter = e => { } } - const barWidth = Math.floor(2.0 / 3 * minSep) + // calculate bar width using some graphics math while + // ensuring a bar is never smaller than one px, so it is always rendered + const barWidth = Math.max(Math.floor(2.0 / 3.0 * minSep), 1.0) const fillColors = [] const strokeColors = g.getColors() + + let selPointX + if (g.selPoints_ && g.selPoints_.length) { + selPointX = g.selPoints_[0].canvasx + } + for (let i = 0; i < strokeColors.length; i++) { fillColors.push(darkenColor(strokeColors[i])) } + ctx.lineWidth = 2 + for (let j = 0; j < sets.length; j++) { - ctx.fillStyle = fillColors[j] ctx.strokeStyle = strokeColors[j] for (let i = 0; i < sets[j].length; i++) { const p = sets[j][i] const centerX = p.canvasx + ctx.fillStyle = fillColors[j] const xLeft = sets.length === 1 - ? centerX - barWidth / 2 - : centerX - barWidth / 2 * (1 - j / (sets.length - 1)) + ? centerX - barWidth + : centerX - barWidth * (1 - j / sets.length) ctx.fillRect( xLeft, @@ -77,12 +87,15 @@ export const multiColumnBarPlotter = e => { yBottom - p.canvasy ) - ctx.strokeRect( - xLeft, - p.canvasy, - barWidth / sets.length, - yBottom - p.canvasy - ) + // hover highlighting + if (selPointX === centerX) { + ctx.strokeRect( + xLeft, + p.canvasy, + barWidth / sets.length, + yBottom - p.canvasy + ) + } } } } diff --git a/ui/src/shared/parsing/getRangeForDygraph.js b/ui/src/shared/parsing/getRangeForDygraph.js index f92566a63b..7a19fa439f 100644 --- a/ui/src/shared/parsing/getRangeForDygraph.js +++ b/ui/src/shared/parsing/getRangeForDygraph.js @@ -1,15 +1,24 @@ const PADDING_FACTOR = 0.1 -export default function getRange( - timeSeries, - override, - ruleValues = {value: null, rangeValue: null} -) { - if (override) { - return override +const considerEmpty = (userNumber, number) => { + if (userNumber === '') { + return null } + if (userNumber) { + return +userNumber + } + + return number +} + +const getRange = ( + timeSeries, + userSelectedRange = [null, null], + ruleValues = {value: null, rangeValue: null} +) => { const {value, rangeValue, operator} = ruleValues + const [userMin, userMax] = userSelectedRange const subtractPadding = val => +val - Math.abs(val * PADDING_FACTOR) const addPadding = val => +val + Math.abs(val * PADDING_FACTOR) @@ -52,10 +61,22 @@ export default function getRange( [null, null] ) + const [min, max] = range + // If time series is such that min and max are equal use Dygraph defaults - if (range[0] === range[1]) { + if (min === max) { return [null, null] } - return range + if (userMin === userMax) { + return [min, max] + } + + if (userMin && userMax) { + return [considerEmpty(userMin), considerEmpty(userMax)] + } + + return [considerEmpty(userMin, min), considerEmpty(userMax, max)] } + +export default getRange diff --git a/ui/src/shared/presenters/index.js b/ui/src/shared/presenters/index.js index 3b1370935a..430d860546 100644 --- a/ui/src/shared/presenters/index.js +++ b/ui/src/shared/presenters/index.js @@ -108,3 +108,9 @@ function getRolesForUser(roles, user) { return buildRoles(userRoles) } + +export const buildYLabel = queryConfig => { + return queryConfig.rawText + ? '' + : `${queryConfig.measurement}.${queryConfig.fields[0].field}` +} diff --git a/ui/src/status/actions/index.js b/ui/src/status/actions/index.js index d985035b30..c64594a678 100644 --- a/ui/src/status/actions/index.js +++ b/ui/src/status/actions/index.js @@ -7,8 +7,6 @@ import {errorThrown} from 'shared/actions/errors' import * as actionTypes from 'src/status/constants/actionTypes' -import {HTTP_NOT_FOUND} from 'shared/constants' - const fetchJSONFeedRequested = () => ({ type: actionTypes.FETCH_JSON_FEED_REQUESTED, }) @@ -45,15 +43,11 @@ export const fetchJSONFeedAsync = url => async dispatch => { } catch (error) { console.error(error) dispatch(fetchJSONFeedFailed()) - if (error.status === HTTP_NOT_FOUND) { - dispatch( - errorThrown( - error, - `Failed to fetch News Feed. JSON Feed at '${url}' returned 404 (Not Found)` - ) + dispatch( + errorThrown( + error, + `Failed to fetch JSON Feed for News Feed from '${url}'` ) - } else { - dispatch(errorThrown(error, 'Failed to fetch NewsFeed')) - } + ) } } diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 1725aadc96..55094c8cf9 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -25,6 +25,7 @@ @import 'layout/flash-messages'; // Components +@import 'components/ceo-display-options'; @import 'components/confirm-buttons'; @import 'components/custom-time-range'; @import 'components/dygraphs'; @@ -47,7 +48,6 @@ @import 'components/tables'; // Pages - @import 'pages/config-endpoints'; @import 'pages/signup'; @import 'pages/auth-page'; diff --git a/ui/src/style/components/ceo-display-options.scss b/ui/src/style/components/ceo-display-options.scss new file mode 100644 index 0000000000..d558f53c3b --- /dev/null +++ b/ui/src/style/components/ceo-display-options.scss @@ -0,0 +1,153 @@ +/* + Cell Editor Overlay - Display Options + ------------------------------------------------------ +*/ +.display-options { + height: 100%; + margin: 0 60px; + display: flex; + background-color: $g2-kevlar; + padding: 0 8px 8px 8px; + border-radius: 0 0 4px 4px; + flex-wrap: nowrap; + align-items: stretch; +} +.display-options--cell { + border-radius: 3px; + background-color: $g3-castle; + padding: 30px; + flex: 1 0 0; + margin-right: 8px; + + &:last-child { margin-right: 0; } +} +.display-options--cellx2 { + flex: 2 0 0; +} +.display-options--header { + margin: 0 0 12px 0; + font-weight: 400; + color: $g11-sidewalk; + @include no-user-select(); +} + + +.viz-type-selector { + display: flex; + flex-wrap: wrap; + height: calc(100% - 22px); + margin: -4px; +} +.viz-type-selector--option { + flex: 1 0 33.3333%; + height: 50%; + padding: 4px; + + > div > p { + margin: 0; + font-size: 14px; + font-weight: 900; + position: absolute; + bottom: 6.25%; + left: 50%; + transform: translate(-50%, 50%); + display: inline-block; + } + + // Actual "card" + > div { + background-color: $g3-castle; + border: 2px solid $g4-onyx; + color: $g11-sidewalk; + border-radius: 3px; + width: 100%; + height: 100%; + display: block; + position: relative; + transition: + color 0.25s ease, + border-color 0.25s ease, + background-color 0.25s ease; + + &:hover { + cursor: pointer; + background-color: $g4-onyx; + border-color: $g5-pepper; + color: $g15-platinum; + } + } +} +// 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%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + + > svg { + transform: translate3d(0,0,0); + width: 100%; + height: 100%; + } +} +.viz-type-selector--graphic-line { + stroke-width: 3px; + fill: none; + stroke-linecap: round; + stroke-miterlimit: 10; + transition: all 0.5s ease; + + &.graphic-line-a {stroke: $g11-sidewalk;} + &.graphic-line-b {stroke: $g9-mountain;} + &.graphic-line-c {stroke: $g7-graphite;} +} +.viz-type-selector--graphic-fill { + opacity: 0.035; + transition: opacity 0.5s ease; + + &.graphic-fill-a {fill: $g11-sidewalk;} &.graphic-fill-b {fill: $g9-mountain;} + &.graphic-fill-c {fill: $g7-graphite;} +} +.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-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;} +} + + + +.display-options--cell .form-group .nav.nav-tablist { + display: flex; + width: 100%; + + > li { + flex: 1 0 0; + justify-content: center; + } +} + +.display-options--footnote { + color: $g11-sidewalk; + margin: 0; + margin-top: 8px; + font-style: italic; + display: inline-block; + width: 100%; + padding-left: 6px; + @include no-user-select(); +} diff --git a/ui/src/style/components/write-data-form.scss b/ui/src/style/components/write-data-form.scss index cb8ede6052..5a76ce6d4f 100644 --- a/ui/src/style/components/write-data-form.scss +++ b/ui/src/style/components/write-data-form.scss @@ -7,7 +7,7 @@ $write-data--max-width: 960px; $write-data--gutter: 30px; $write-data--margin: 18px; -$write-data--input-height: 120px; +$write-data--input-height: calc(90vh - 48px - 60px - 36px); // Heights of everything but input height $write-data--transition: opacity 0.4s ease; .write-data-form {