Merge branch 'master' into feature/1685-moving-time-ranges

pull/10616/head
Hunter Trujillo 2017-08-07 16:53:34 -07:00
commit c315d9b142
38 changed files with 1474 additions and 325 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

37
mocks/dashboards.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<div
className="alert-history-table--tr"
key={`${name}-${level}-${time}-${host}-${value}`}
>
<div className="alert-history-table--tr" key={uuid.v4()}>
<div
className="alert-history-table--td"
style={{width: colName}}

View File

@ -0,0 +1,103 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
// TODO: add logic for for Prefix, Suffix, Scale, and Multiplier
const AxesOptions = ({onSetRange, onSetLabel, axes}) => {
const min = _.get(axes, ['y', 'bounds', '0'], '')
const max = _.get(axes, ['y', 'bounds', '1'], '')
const label = _.get(axes, ['y', 'label'], '')
return (
<div className="display-options--cell">
<h5 className="display-options--header">Y Axis Controls</h5>
<form autoComplete="off" style={{margin: '0 -6px'}}>
<div className="form-group col-sm-12">
<label htmlFor="prefix">Title</label>
<input
className="form-control input-sm"
type="text"
name="label"
id="label"
value={label}
onChange={onSetLabel}
placeholder="auto"
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="min">Min</label>
<input
className="form-control input-sm"
type="number"
name="min"
id="min"
value={min}
onChange={onSetRange}
placeholder="auto"
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="max">Max</label>
<input
className="form-control input-sm"
type="number"
name="max"
id="max"
value={max}
onChange={onSetRange}
placeholder="auto"
/>
</div>
<p className="display-options--footnote">
Values left blank will be set automatically
</p>
{/* <div className="form-group col-sm-6">
<label htmlFor="prefix">Labels Prefix</label>
<input
className="form-control input-sm"
type="text"
name="prefix"
id="prefix"
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="prefix">Labels Suffix</label>
<input
className="form-control input-sm"
type="text"
name="suffix"
id="suffix"
/>
</div>
<div className="form-group col-sm-6">
<label>Labels Format</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li className="active">K/M/B</li>
<li>K/M/G</li>
</ul>
</div>
<div className="form-group col-sm-6">
<label>Scale</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li className="active">Linear</li>
<li>Logarithmic</li>
</ul>
</div> */}
</form>
</div>
)
}
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

View File

@ -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={[]}
/>
<div className="overlay-technology--editor">
<OverlayControls
selectedGraphType={cellWorkingType}
onSelectGraphType={this.handleSelectGraphType}
isDisplayOptionsTabActive={isDisplayOptionsTabActive}
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
onCancel={onCancel}
onSave={this.handleSaveCell}
isSavable={queriesWorkingDraft.every(isQuerySavable)}
/>
<QueryMaker
source={source}
templates={templates}
queries={queriesWorkingDraft}
actions={queryActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
activeQueryIndex={activeQueryIndex}
/>
{isDisplayOptionsTabActive
? <DisplayOptions
selectedGraphType={cellWorkingType}
onSelectGraphType={this.handleSelectGraphType}
onSetRange={this.handleSetYAxisBounds}
onSetLabel={this.handleSetLabel}
axes={axes}
/>
: <QueryMaker
source={source}
templates={templates}
queries={queriesWorkingDraft}
actions={queryActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
activeQueryIndex={activeQueryIndex}
/>}
</div>
</ResizeContainer>
</div>
@ -232,6 +294,7 @@ CellEditorOverlay.propTypes = {
queryID: string,
status: shape({}),
}).isRequired,
dashboardID: string.isRequired,
}
export default CellEditorOverlay

View File

@ -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,
}) =>
<div className="display-options">
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
<AxesOptions onSetLabel={onSetLabel} onSetRange={onSetRange} axes={axes} />
</div>
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

View File

@ -0,0 +1,35 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import {graphTypes} from 'src/dashboards/graphics/graph'
const GraphTypeSelector = ({selectedGraphType, onSelectGraphType}) =>
<div className="display-options--cell display-options--cellx2">
<h5 className="display-options--header">Visualization Type</h5>
<div className="viz-type-selector">
{graphTypes.map(graphType =>
<div
key={graphType.type}
className={classnames('viz-type-selector--option', {
active: graphType.type === selectedGraphType,
})}
>
<div onClick={() => onSelectGraphType(graphType.type)}>
{graphType.graphic}
<p>
{graphType.menuOption}
</p>
</div>
</div>
)}
</div>
</div>
const {func, string} = PropTypes
GraphTypeSelector.propTypes = {
selectedGraphType: string.isRequired,
onSelectGraphType: func.isRequired,
}
export default GraphTypeSelector

View File

@ -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 (
<div className="overlay-controls">
<h3 className="overlay--graph-name">Cell Editor</h3>
<div className="overlay-controls--right">
<p>Visualization Type:</p>
<ul className="nav nav-tablist nav-tablist-sm">
{graphTypes.map(graphType =>
<li
key={graphType.type}
className={classnames({
active: graphType.type === selectedGraphType,
})}
onClick={() => onSelectGraphType(graphType.type)}
>
{graphType.menuOption}
</li>
)}
</ul>
<ConfirmButtons
onCancel={onCancel}
onConfirm={onSave}
isDisabled={!isSavable}
/>
</div>
const OverlayControls = ({
onCancel,
onSave,
isDisplayOptionsTabActive,
onClickDisplayOptions,
isSavable,
}) =>
<div className="overlay-controls">
<h3 className="overlay--graph-name">Cell Editor</h3>
<ul className="nav nav-tablist nav-tablist-sm">
<li
key="queries"
className={classnames({
active: !isDisplayOptionsTabActive,
})}
onClick={onClickDisplayOptions(false)}
>
Queries
</li>
<li
key="displayOptions"
className={classnames({
active: isDisplayOptionsTabActive,
})}
onClick={onClickDisplayOptions(true)}
>
Display Options
</li>
</ul>
<div className="overlay-controls--right">
<ConfirmButtons
onCancel={onCancel}
onConfirm={onSave}
isDisabled={!isSavable}
/>
</div>
)
}
</div>
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,
}

View File

@ -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
? <CellEditorOverlay
source={source}
dashboardID={dashboardID}
templates={templatesIncludingDashTime}
cell={selectedCell}
timeRange={timeRange}
@ -372,8 +377,8 @@ class DashboardPage extends Component {
autoRefresh={autoRefresh}
synchronizer={this.synchronizer}
onAddCell={this.handleAddCell}
inPresentationMode={inPresentationMode}
onEditCell={this.handleEditDashboardCell}
inPresentationMode={inPresentationMode}
onPositionChange={this.handleUpdatePosition}
onDeleteCell={this.handleDeleteDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}

View File

@ -0,0 +1,266 @@
import React from 'react'
export const graphTypes = [
{
type: 'line',
menuOption: 'Line',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="Line"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<polyline
className="viz-type-selector--graphic-line graphic-line-a"
points="5,122.2 63,81.2 121,95.5 179,40.2 237,108.5 295,83.2"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-a"
points="5,122.2 5,145 295,145 295,83.2 237,108.5 179,40.2 121,95.5 63,81.2"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-b"
points="5,88.5 63,95 121,36.2 179,19 237,126.2 295,100.8"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-b"
points="5,88.5 5,145 295,145 295,100.8 237,126.2 179,19 121,36.2 63,95"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-c"
points="5,76.2 63,90.2 121,59.2 179,31.5 237,79.8 295,93.5"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-c"
points="5,76.2 5,145 295,145 295,93.5 237,79.8 179,31.5 121,59.2 63,90.2"
/>
</svg>
</div>
),
},
{
type: 'line-stacked',
menuOption: 'Stacked',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="LineStacked"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<polyline
className="viz-type-selector--graphic-line graphic-line-a"
points="5,97.5 63,111.8 121,36.2 179,51 237,102.9 295,70.2"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-b"
points="5,58.8 63,81.2 121,5 179,40.2 237,96.2 295,49"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-c"
points="5,107.5 63,128.5 121,79.8 179,76.5 237,113.2 295,93.5"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-a"
points="179,51 121,36.2 63,111.8 5,97.5 5,107.5 63,128.5 121,79.8 179,76.5 237,113.2 295,93.5 295,70.2 237,102.9"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-b"
points="237,96.2 179,40.2 121,5 63,81.2 5,58.8 5,97.5 63,111.8 121,36.2 179,51 237,102.9 295,70.2 295,49"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-c"
points="179,76.5 121,79.8 63,128.5 5,107.5 5,145 295,145 295,93.5 237,113.2"
/>
</svg>
</div>
),
},
{
type: 'line-stepplot',
menuOption: 'Step-Plot',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="StepPlot"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-a"
points="295,85.5 266,85.5 266,108.5 208,108.5 208,94.5 150,94.5 150,41 92,41 92,66.6 34,66.6 34,54.8 5,54.8 5,145 295,145"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-a"
points="5,54.8 34,54.8 34,66.6 92,66.6 92,41 150,41 150,94.5 208,94.5 208,108.5 266,108.5 266,85.5 295,85.5"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-b"
points="34,111 34,85.8 92,85.8 92,5 150,5 150,24.5 208,24.5 208,128.2 266,128.2 266,75 295,75 295,145 5,145 5,111"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-b"
points="5,111 34,111 34,85.8 92,85.8 92,5 150,5 150,24.5 208,24.5 208,128.2 266,128.2 266,75 295,75"
/>
</svg>
</div>
),
},
{
type: 'single-stat',
menuOption: 'SingleStat',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="SingleStat"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M243.3,39.6h-37.9v32.7c0-6.3,5.1-11.4,11.4-11.4h15.2c6.3,0,11.4,5.1,11.4,11.4v26.8c0,6.3-5.1,11.4-11.4,11.4 h-15.2c-6.3,0-11.4-5.1-11.4-11.4V88.6"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-a"
points="94.6,89.1 56.7,89.1 83.2,39.6 83.2,110.4 "
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M144.2,77.8c0,6.3-5.1,11.4-11.4,11.4h-15.2c-6.3,0-11.4-5.1-11.4-11.4V50.9c0-6.3,5.1-11.4,11.4-11.4h15.2 c6.3,0,11.4,5.1,11.4,11.4v48.1c0,6.3-5.1,11.4-11.4,11.4h-15.2c-6.3,0-11.4-5.1-11.4-11.4"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M155.8,50.9c0-6.3,5.1-11.4,11.4-11.4h15.2c6.3,0,11.4,5.1,11.4,11.4c0,24.1-37.9,24.8-37.9,59.5h37.9"
/>
</svg>
</div>
),
},
{
type: 'line-plus-single-stat',
menuOption: 'Line + Stat',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="LineAndSingleStat"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-b"
points="5,122.2 5,145 295,145 295,38.3 237,41.3 179,50 121,126.3 63,90.7"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-b"
points="5,122.2 63,90.7 121,126.3 179,50 237,41.3 295,38.3"
/>
<polygon
className="viz-type-selector--graphic-fill graphic-fill-c"
points="5,26.2 5,145 295,145 295,132.3 239.3,113.3 179,15 121,25 63,71.7"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-c"
points="5,26.2 63,71.7 121,25 179,15 239.3,113.3 295,132.3"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M243.3,39.6h-37.9v32.7c0-6.3,5.1-11.4,11.4-11.4h15.2c6.3,0,11.4,5.1,11.4,11.4v26.8 c0,6.3-5.1,11.4-11.4,11.4h-15.2c-6.3,0-11.4-5.1-11.4-11.4V88.6"
/>
<polyline
className="viz-type-selector--graphic-line graphic-line-a"
points="94.6,89.1 56.7,89.1 83.2,39.6 83.2,110.4"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M144.2,77.8c0,6.3-5.1,11.4-11.4,11.4h-15.2c-6.3,0-11.4-5.1-11.4-11.4V50.9c0-6.3,5.1-11.4,11.4-11.4h15.2 c6.3,0,11.4,5.1,11.4,11.4v48.1c0,6.3-5.1,11.4-11.4,11.4h-15.2c-6.3,0-11.4-5.1-11.4-11.4"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M155.8,50.9c0-6.3,5.1-11.4,11.4-11.4h15.2c6.3,0,11.4,5.1,11.4,11.4c0,24.1-37.9,24.8-37.9,59.5h37.9"
/>
</svg>
</div>
),
},
{
type: 'bar',
menuOption: 'Bar',
graphic: (
<div className="viz-type-selector--graphic">
<svg
version="1.1"
id="Bar"
x="0px"
y="0px"
viewBox="0 0 300 150"
preserveAspectRatio="none meet"
>
<path
className="viz-type-selector--graphic-fill graphic-fill-a"
d="M145,7c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v136c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V7z"
/>
<path
className="viz-type-selector--graphic-fill graphic-fill-c"
d="M195,57c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v86c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V57z"
/>
<path
className="viz-type-selector--graphic-fill graphic-fill-b"
d="M245,117c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v26c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V117z"
/>
<path
className="viz-type-selector--graphic-fill graphic-fill-a"
d="M295,107c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v36c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V107z"
/>
<path
className="viz-type-selector--graphic-fill graphic-fill-b"
d="M95,87c0-1.1-0.9-2-2-2H57c-1.1,0-2,0.9-2,2v56c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V87z"
/>
<path
className="viz-type-selector--graphic-fill graphic-fill-c"
d="M45,130c0-1.1-0.9-2-2-2H7c-1.1,0-2,0.9-2,2v13c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V130z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M145,7c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v136c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V7z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-c"
d="M195,57c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v86c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V57z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-b"
d="M245,117c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v26c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V117z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-a"
d="M295,107c0-1.1-0.9-2-2-2h-36c-1.1,0-2,0.9-2,2v36c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V107z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-b"
d="M95,87c0-1.1-0.9-2-2-2H57c-1.1,0-2,0.9-2,2v56c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V87z"
/>
<path
className="viz-type-selector--graphic-line graphic-line-c"
d="M45,130c0-1.1-0.9-2-2-2H7c-1.1,0-2,0.9-2,2v13c0,1.1,0.9,2,2,2h36c1.1,0,2-0.9,2-2V130z"
/>
</svg>
</div>
),
},
]

View File

@ -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 (
<RefreshingSingleStat
queries={queries.length ? [queries[0]] : []}
autoRefresh={autoRefresh}
templates={templates}
/>
)
}
const displayOptions = {
stepPlot: cellType === 'line-stepplot',
stackedGraph: cellType === 'line-stacked',
}
return (
<RefreshingLineGraph
<RefreshingGraph
axes={axes}
type={cellType}
queries={queries}
autoRefresh={autoRefresh}
templates={templates}
activeQueryIndex={activeQueryIndex}
isInDataExplorer={isInDataExplorer}
showSingleStat={cellType === 'line-plus-single-stat'}
isBarGraph={cellType === 'bar'}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
cellHeight={heightPixels}
autoRefresh={autoRefresh}
/>
)
}
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

View File

@ -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({
>
<VisView
view={view}
axes={axes}
queries={queries}
templates={templates}
cellType={cellType}

View File

@ -33,7 +33,7 @@ const WriteDataBody = ({
ref={fileInput}
accept="text/*, application/gzip"
/>
<button className="btn btn-sm btn-primary" onClick={handleFileOpen}>
<button className="btn btn-md btn-primary" onClick={handleFileOpen}>
{uploadContent ? 'Upload a Different File' : 'Upload a File'}
</button>
{uploadContent

View File

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

View File

@ -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 {
<div
ref={r => {
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,
}

View File

@ -151,7 +151,7 @@ class LayoutRenderer extends Component {
} = this.props
return cells.map(cell => {
const {type, h} = cell
const {type, h, axes} = cell
return (
<div key={cell.i}>
@ -175,6 +175,7 @@ class LayoutRenderer extends Component {
type={type}
queries={this.standardizeQueries(cell, source)}
cellHeight={h}
axes={axes}
/>}
</NameableGraph>
</div>

View File

@ -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 (
<div
className={classnames('dygraph', {
'graph--hasYLabel': !!(options.ylabel || options.y2label),
})}
style={{height: '100%'}}
>
<div className={`dygraph ${this.yLabelClass()}`} style={{height: '100%'}}>
{isRefreshing ? this.renderSpinner() : null}
<Dygraph
axes={axes}
queries={queries}
dygraphRef={this.dygraphRefFunc}
containerStyle={{width: '100%', height: '100%'}}
overrideLineColors={
showSingleStat ? singleStatLineColors : overrideLineColors
}
overrideLineColors={lineColors}
isGraphFilled={showSingleStat ? false : isGraphFilled}
isBarGraph={isBarGraph}
timeSeries={timeSeries}
labels={labels}
options={showSingleStat ? singleStatOptions : options}
dygraphSeries={dygraphSeries}
ranges={ranges || this.getRanges()}
ruleValues={ruleValues}
synchronizer={synchronizer}
timeRange={timeRange}
@ -193,6 +195,26 @@ export default React.createClass({
)
},
yLabelClass() {
const dygraph = this.dygraphRef
if (!dygraph) {
return 'graph--hasYLabel'
}
const label = dygraph.querySelector('.dygraph-ylabel')
if (!label) {
return ''
}
return 'graph--hasYLabel'
},
dygraphRefFunc(dygraphRef) {
this.dygraphRef = dygraphRef
},
renderSpinner() {
return (
<div className="graph-panel__refreshing">
@ -202,25 +224,4 @@ export default React.createClass({
</div>
)
},
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
},
})

View File

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

View File

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

View File

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

View File

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

View File

@ -108,3 +108,9 @@ function getRolesForUser(roles, user) {
return buildRoles(userRoles)
}
export const buildYLabel = queryConfig => {
return queryConfig.rawText
? ''
: `${queryConfig.measurement}.${queryConfig.fields[0].field}`
}

View File

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

View File

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

View File

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

View File

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