Merge branch 'master' into fix/ie11-support

pull/10616/head
Hunter Trujillo 2017-08-14 12:47:58 -06:00 committed by GitHub
commit eb0ab9523b
47 changed files with 1547 additions and 366 deletions

View File

@ -1,12 +1,33 @@
## v1.3.6.0 [unreleased]
## v1.3.7.0 [unreleased]
### Bug Fixes
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix no-scroll bar appearing in the Data Explorer table
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix missing cell type (and consequently single-stat)
### Features
### UI Improvements
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
## v1.3.5.0 [2017-07-25]
### UI Improvements
1. [#1846](https://github.com/influxdata/chronograf/pull/1846): Increase screen real estate of Query Maker in the Cell Editor Overlay
## v1.3.6.0 [2017-08-08]
### 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's synchronize method 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. [#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 write data modal to indicate data is being written
1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Fix bar graphs overlapping
1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Assign a series consistent coloring when it appears in multiple cells
1. [#1800](https://github.com/influxdata/chronograf/pull/1800): Increase size of 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-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

@ -21,7 +21,7 @@ We really like to receive feature requests, as it helps us prioritize our work.
Contributing to the source code
-------------------------------
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses NPM for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses Yarn for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
Submitting a pull request
-------------------------
@ -43,9 +43,9 @@ Signing the CLA
If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found
[on our website](https://influxdata.com/community/cla/).
Installing NPM
Installing Yarn
--------------
You'll need to install NPM to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the NPM downloads page](https://nodejs.org/en/download/).
You'll need to install Yarn to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the Yarn installation page](https://yarnpkg.com/en/docs/install).
Installing Go
-------------
@ -105,7 +105,7 @@ Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessa
Build and Test
--------------
Make sure you have `go` and `npm` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following:
Make sure you have `go` and `yarn` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following:
```bash
cd $GOPATH/src/github.com/influxdata/chronograf

View File

@ -60,11 +60,11 @@ canned/bin_gen.go: canned/*.json
go generate -x ./canned
.jssrc: $(UISOURCES)
cd ui && npm run build
cd ui && yarn run build
@touch .jssrc
.dev-jssrc: $(UISOURCES)
cd ui && npm run build:dev
cd ui && yarn run build:dev
@touch .dev-jssrc
dep: .jsdep .godep
@ -98,7 +98,7 @@ gotestrace:
go test -race `go list ./... | grep -v /vendor/`
jstest:
cd ui && npm test
cd ui && yarn test
run: ${BINARY}
./chronograf
@ -108,7 +108,7 @@ run-dev: chronogiraffe
clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
cd ui && npm run clean
cd ui && yarn run clean
cd ui && rm -rf node_modules
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata

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,35 @@ 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
newCell.Type = cell.Type
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,139 @@ 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",
Type: "bar",
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",
Type: "bar",
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

@ -50,7 +50,7 @@ type Server struct {
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"hunter2\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`

View File

@ -33,4 +33,4 @@ yarn upgrade packageName
```
## Testing
Tests can be run via command line with `npm test`, from within the `/ui` directory. For more detailed reporting, use `npm test -- --reporters=verbose`.
Tests can be run via command line with `yarn test`, from within the `/ui` directory. For more detailed reporting, use `yarn test -- --reporters=verbose`.

View File

@ -16,7 +16,7 @@ module.exports = function(config) {
'spec/index.js': ['webpack', 'sourcemap'],
},
// For more detailed reporting on tests, you can add 'verbose' and/or 'progress'.
// This can also be done via the command line with `npm test -- --reporters=verbose`.
// This can also be done via the command line with `yarn test -- --reporters=verbose`.
reporters: ['dots'],
webpack: {
devtool: 'inline-source-map',

View File

@ -1,6 +1,6 @@
{
"name": "chronograf-ui",
"version": "1.3.5-0",
"version": "1.3.6-0",
"private": false,
"license": "AGPL-3.0",
"description": "",
@ -9,13 +9,13 @@
"url": "github:influxdata/chronograf"
},
"scripts": {
"build": "npm run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js",
"build": "yarn run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js",
"build:dev": "node_modules/webpack/bin/webpack.js --config ./webpack/devConfig.js",
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
"lint": "node_modules/eslint/bin/eslint.js src/",
"test": "karma start",
"test:lint": "npm run lint; npm run test",
"test:dev": "nodemon --exec npm run test:lint",
"test:lint": "yarn run lint; yarn run test",
"test:dev": "nodemon --exec yarn run test:lint",
"clean": "rm -rf build",
"storybook": "node ./storybook",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"

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

@ -12,7 +12,6 @@ import {Table, Column, Cell} from 'fixed-data-table'
const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes
const defaultTableHeight = 1000
const emptySeries = {columns: [], values: []}
const CustomCell = React.createClass({
@ -64,7 +63,7 @@ const ChronoTable = React.createClass({
getDefaultProps() {
return {
height: defaultTableHeight,
height: 500,
}
},
@ -139,11 +138,11 @@ const ChronoTable = React.createClass({
const maximumTabsCount = 11
// adjust height to proper value by subtracting the heights of the UI around it
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
const stylePixelOffset = 136
const rowHeight = 34
const defaultColumnWidth = 200
const headerHeight = 30
const minWidth = 70
const rowHeight = 34
const headerHeight = 30
const stylePixelOffset = 125
const defaultColumnWidth = 200
const styleAdjustedHeight = height - stylePixelOffset
const width =
columns && columns.length > 1 ? defaultColumnWidth : containerWidth

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,7 @@ const VisView = ({
heightPixels,
editQueryStatus,
activeQueryIndex,
isInDataExplorer,
resizerBottomHeight,
}) => {
const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0]
@ -34,46 +31,30 @@ const VisView = ({
return (
<Table
query={query}
height={heightPixels}
height={resizerBottomHeight}
editQueryStatus={editQueryStatus}
/>
)
}
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}
cellHeight={heightPixels}
autoRefresh={autoRefresh}
editQueryStatus={editQueryStatus}
/>
)
}
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 +62,7 @@ VisView.propTypes = {
heightPixels: number,
editQueryStatus: func.isRequired,
activeQueryIndex: number,
isInDataExplorer: bool,
resizerBottomHeight: number,
}
export default VisView

View File

@ -26,6 +26,12 @@ const Visualization = React.createClass({
heightPixels: number,
editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
}),
}),
resizerBottomHeight: number,
},
contextTypes: {
@ -48,6 +54,7 @@ const Visualization = React.createClass({
getDefaultProps() {
return {
cellName: '',
cellType: '',
}
},
@ -76,6 +83,7 @@ const Visualization = React.createClass({
render() {
const {
axes,
views,
height,
cellType,
@ -88,16 +96,19 @@ const Visualization = React.createClass({
editQueryStatus,
activeQueryIndex,
isInDataExplorer,
resizerBottomHeight,
} = this.props
const {source: {links: {proxy}}} = this.context
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 +127,7 @@ const Visualization = React.createClass({
>
<VisView
view={view}
axes={axes}
queries={queries}
templates={templates}
cellType={cellType}
@ -124,6 +136,7 @@ const Visualization = React.createClass({
editQueryStatus={editQueryStatus}
activeQueryIndex={activeQueryIndex}
isInDataExplorer={isInDataExplorer}
resizerBottomHeight={resizerBottomHeight}
/>
</div>
</div>

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

@ -8,13 +8,15 @@ const RefreshingLineGraph = AutoRefresh(LineGraph)
const RefreshingSingleStat = AutoRefresh(SingleStat)
const RefreshingGraph = ({
timeRange,
autoRefresh,
templates,
synchronizer,
axes,
type,
queries,
templates,
timeRange,
cellHeight,
autoRefresh,
synchronizer,
editQueryStatus,
}) => {
if (type === 'single-stat') {
return (
@ -42,6 +44,8 @@ const RefreshingGraph = ({
isBarGraph={type === 'bar'}
displayOptions={displayOptions}
synchronizer={synchronizer}
editQueryStatus={editQueryStatus}
axes={axes}
/>
)
}
@ -56,8 +60,10 @@ RefreshingGraph.propTypes = {
templates: arrayOf(shape()),
synchronizer: func,
type: string.isRequired,
cellHeight: number,
axes: shape(),
queries: arrayOf(shape()).isRequired,
cellHeight: number.isRequired,
editQueryStatus: func,
}
export default RefreshingGraph

View File

@ -31,6 +31,12 @@ class ResizeContainer extends Component {
initialBottomHeight: defaultInitialBottomHeight,
}
componentDidMount() {
this.setState({
bottomHeightPixels: this.bottom.getBoundingClientRect().height,
})
}
handleStartDrag() {
this.setState({isDragging: true})
}
@ -51,7 +57,7 @@ class ResizeContainer extends Component {
const {minTopHeight, minBottomHeight} = this.props
const oneHundred = 100
const containerHeight = parseInt(
getComputedStyle(this.refs.resizeContainer).height,
getComputedStyle(this.resizeContainer).height,
10
)
// verticalOffset moves the resize handle as many pixels as the page-heading is taking up.
@ -85,11 +91,12 @@ class ResizeContainer extends Component {
this.setState({
topHeight: `${newTopPanelPercent}%`,
bottomHeight: `${newBottomPanelPercent}%`,
bottomHeightPixels,
})
}
render() {
const {topHeight, bottomHeight, isDragging} = this.state
const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state
const {containerClass, children} = this.props
if (React.Children.count(children) > maximumNumChildren) {
@ -107,10 +114,12 @@ class ResizeContainer extends Component {
onMouseLeave={this.handleMouseLeave}
onMouseUp={this.handleStopDrag}
onMouseMove={this.handleDrag}
ref="resizeContainer"
ref={r => (this.resizeContainer = r)}
>
<div className="resize--top" style={{height: topHeight}}>
{React.cloneElement(children[0])}
{React.cloneElement(children[0], {
resizerBottomHeight: bottomHeightPixels,
})}
</div>
<ResizeHandle
isDragging={isDragging}
@ -120,8 +129,11 @@ class ResizeContainer extends Component {
<div
className="resize--bottom"
style={{height: bottomHeight, top: topHeight}}
ref={r => (this.bottom = r)}
>
{React.cloneElement(children[1])}
{React.cloneElement(children[1], {
resizerBottomHeight: bottomHeightPixels,
})}
</div>
</div>
)

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,151 @@
/*
Cell Editor Overlay - Display Options
------------------------------------------------------
*/
.display-options {
height: 100%;
display: flex;
background-color: $g2-kevlar;
padding: 0 18px 8px 18px;
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 {

View File

@ -3,7 +3,7 @@
------------------------------------------------------
*/
$overlay-controls-height: 50px;
$overlay-controls-height: 60px;
$overlay-controls-bg: $g2-kevlar;
$overlay-z: 100;
@ -33,17 +33,16 @@ $overlay-z: 100;
}
.overlay-controls {
padding: 0 16px;
padding: 0 18px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
flex: 0 0 $overlay-controls-height;
width: calc(100% - #{($page-wrapper-padding * 2)});
left: $page-wrapper-padding;
width: 100%;
left: 0;
border: 0;
background-color: $g2-kevlar;
border-radius: $radius $radius 0 0;
}
.overlay-controls--right {
display: flex;
@ -74,7 +73,9 @@ $overlay-z: 100;
flex-direction: column;
align-items: stretch;
height: 100%;
padding: 16px 0;
}
.overlay-technology--editor .query-maker--empty {
margin-bottom: 8px;
}
.overlay-controls .confirm-buttons {
margin-left: 32px;
@ -86,8 +87,8 @@ $overlay-z: 100;
}
.overlay-technology .query-maker {
flex: 1 0 0%;
padding: 0 8px;
border-radius: 0 0 $radius $radius;
padding: 0 18px;
margin: 0;
background-color: $g2-kevlar;
}
.overlay-technology .query-maker--tabs {