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 ### Bug Fixes
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11. 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 ### Features
### UI Improvements 1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written
## 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 ### Bug Fixes
1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu 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 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 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 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 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/). [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 Installing Go
------------- -------------
@ -105,7 +105,7 @@ Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessa
Build and Test 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 ```bash
cd $GOPATH/src/github.com/influxdata/chronograf cd $GOPATH/src/github.com/influxdata/chronograf

View File

@ -60,11 +60,11 @@ canned/bin_gen.go: canned/*.json
go generate -x ./canned go generate -x ./canned
.jssrc: $(UISOURCES) .jssrc: $(UISOURCES)
cd ui && npm run build cd ui && yarn run build
@touch .jssrc @touch .jssrc
.dev-jssrc: $(UISOURCES) .dev-jssrc: $(UISOURCES)
cd ui && npm run build:dev cd ui && yarn run build:dev
@touch .dev-jssrc @touch .dev-jssrc
dep: .jsdep .godep dep: .jsdep .godep
@ -98,7 +98,7 @@ gotestrace:
go test -race `go list ./... | grep -v /vendor/` go test -race `go list ./... | grep -v /vendor/`
jstest: jstest:
cd ui && npm test cd ui && yarn test
run: ${BINARY} run: ${BINARY}
./chronograf ./chronograf
@ -108,7 +108,7 @@ run-dev: chronogiraffe
clean: clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
cd ui && npm run clean cd ui && yarn run clean
cd ui && rm -rf node_modules cd ui && rm -rf node_modules
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata @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)) axes := make(map[string]*Axis, len(c.Axes))
for a, r := range 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{ 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)) axes := make(map[string]chronograf.Axis, len(c.Axes))
for a, r := range c.Axes { for a, r := range c.Axes {
axis := chronograf.Axis{} if r.Bounds != nil {
copy(axis.Bounds[:], r.Bounds[:2]) axes[a] = chronograf.Axis{
axes[a] = axis Bounds: r.Bounds,
Label: r.Label,
}
} else {
axes[a] = chronograf.Axis{
Bounds: []string{},
}
}
} }
cells[i] = chronograf.DashboardCell{ cells[i] = chronograf.DashboardCell{

View File

@ -118,7 +118,9 @@ func (m *DashboardCell) GetAxes() map[string]*Axis {
} }
type Axis struct { 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{} } func (m *Axis) Reset() { *m = Axis{} }
@ -313,65 +315,66 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{ var fileDescriptorInternal = []byte{
// 952 bytes of a gzipped FileDescriptorProto // 974 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xdf, 0x6e, 0xe3, 0xc4,
0x13, 0x56, 0xc7, 0x76, 0x12, 0x57, 0x66, 0xe7, 0xf7, 0x53, 0x6b, 0xc5, 0x9a, 0x45, 0x42, 0xc1, 0x17, 0xd6, 0xc4, 0x76, 0x12, 0x9f, 0x76, 0xfb, 0xfb, 0x69, 0xb4, 0x62, 0xcd, 0x72, 0x13, 0x2c,
0x02, 0x29, 0x20, 0x31, 0xa0, 0x5d, 0x21, 0x21, 0x6e, 0x99, 0x09, 0x5a, 0x85, 0x99, 0x5d, 0x86, 0x90, 0x02, 0x12, 0x05, 0xed, 0x0a, 0x09, 0x71, 0x97, 0x36, 0x68, 0x55, 0xda, 0x5d, 0xca, 0xa4,
0xce, 0xcc, 0x70, 0x42, 0xab, 0x4e, 0x52, 0x99, 0x58, 0xeb, 0xc4, 0xa6, 0x6d, 0x4f, 0xe2, 0xb7, 0x2d, 0xdc, 0xa0, 0xd5, 0xc4, 0x39, 0x4d, 0xac, 0x75, 0x62, 0x33, 0xb6, 0x9b, 0xf8, 0x2d, 0x78,
0xe0, 0x09, 0x90, 0x90, 0x38, 0x71, 0xe0, 0xc0, 0x0b, 0xf0, 0x10, 0xbc, 0x10, 0xaa, 0xee, 0xf6, 0x02, 0x24, 0x24, 0xae, 0xb8, 0xe0, 0x82, 0x17, 0xe0, 0x21, 0x78, 0x21, 0x74, 0x66, 0xc6, 0x7f,
0x9f, 0xb0, 0xb3, 0x68, 0x4f, 0xdc, 0xfa, 0xab, 0xea, 0x7c, 0xe5, 0xfe, 0xea, 0xab, 0x52, 0xe0, 0xc2, 0x76, 0xd1, 0x5e, 0x71, 0x37, 0xdf, 0x39, 0xe3, 0x6f, 0x66, 0xbe, 0xf3, 0x9d, 0x23, 0xc3,
0x38, 0xda, 0xe6, 0xa8, 0xb6, 0x32, 0x3e, 0x49, 0x55, 0x92, 0x27, 0xbc, 0x5f, 0xe1, 0xf0, 0xf7, 0x51, 0xbc, 0x29, 0x50, 0x6d, 0x64, 0x72, 0x9c, 0xa9, 0xb4, 0x48, 0xf9, 0xb0, 0xc6, 0xe1, 0xef,
0x0e, 0x74, 0x67, 0x49, 0xa1, 0x16, 0xc8, 0x8f, 0xa1, 0x33, 0x9d, 0x04, 0x6c, 0xc8, 0x46, 0x8e, 0x3d, 0xe8, 0xcf, 0xd2, 0x52, 0x45, 0xc8, 0x8f, 0xa0, 0x77, 0x36, 0x0d, 0xd8, 0x88, 0x8d, 0x1d,
0xe8, 0x4c, 0x27, 0x9c, 0x83, 0xfb, 0x42, 0x6e, 0x30, 0xe8, 0x0c, 0xd9, 0xc8, 0x17, 0xfa, 0x4c, 0xd1, 0x3b, 0x9b, 0x72, 0x0e, 0xee, 0x0b, 0xb9, 0xc6, 0xa0, 0x37, 0x62, 0x63, 0x5f, 0xe8, 0x35,
0xb1, 0xab, 0x32, 0xc5, 0xc0, 0x31, 0x31, 0x3a, 0xf3, 0xc7, 0xd0, 0xbf, 0xce, 0x88, 0x6d, 0x83, 0xc5, 0xae, 0xaa, 0x0c, 0x03, 0xc7, 0xc4, 0x68, 0xcd, 0x1f, 0xc3, 0xf0, 0x3a, 0x27, 0xb6, 0x35,
0x81, 0xab, 0xe3, 0x35, 0xa6, 0xdc, 0xa5, 0xcc, 0xb2, 0x5d, 0xa2, 0x96, 0x81, 0x67, 0x72, 0x15, 0x06, 0xae, 0x8e, 0x37, 0x98, 0x72, 0x97, 0x32, 0xcf, 0xb7, 0xa9, 0x5a, 0x04, 0x9e, 0xc9, 0xd5,
0xe6, 0xff, 0x07, 0xe7, 0x5a, 0x5c, 0x04, 0x5d, 0x1d, 0xa6, 0x23, 0x0f, 0xa0, 0x37, 0xc1, 0x95, 0x98, 0xff, 0x1f, 0x9c, 0x6b, 0x71, 0x11, 0xf4, 0x75, 0x98, 0x96, 0x3c, 0x80, 0xc1, 0x14, 0x6f,
0x2c, 0xe2, 0x3c, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x2a, 0x48, 0x3c, 0x57, 0x18, 0xe3, 0xad, 0x92, 0x65, 0x99, 0x14, 0xc1, 0x60, 0xc4, 0xc6, 0x43, 0x51, 0x43, 0xe2, 0xb9, 0xc2, 0x04, 0x97, 0x4a,
0xab, 0xa0, 0x6f, 0x78, 0x2a, 0xcc, 0x4f, 0x80, 0x4f, 0xb7, 0x19, 0x2e, 0x0a, 0x85, 0xb3, 0x57, 0xde, 0x06, 0x43, 0xc3, 0x53, 0x63, 0x7e, 0x0c, 0xfc, 0x6c, 0x93, 0x63, 0x54, 0x2a, 0x9c, 0xbd,
0x51, 0x7a, 0x83, 0x2a, 0x5a, 0x95, 0x81, 0xaf, 0x09, 0xee, 0xc9, 0x50, 0x95, 0xe7, 0x98, 0x4b, 0x8a, 0xb3, 0x1b, 0x54, 0xf1, 0x6d, 0x15, 0xf8, 0x9a, 0xe0, 0x9e, 0x0c, 0x9d, 0xf2, 0x1c, 0x0b,
0xaa, 0x0d, 0x9a, 0xaa, 0x82, 0x3c, 0x84, 0xa3, 0xd9, 0x5a, 0x2a, 0x5c, 0xce, 0x70, 0xa1, 0x30, 0x49, 0x67, 0x83, 0xa6, 0xaa, 0x21, 0x0f, 0xe1, 0x70, 0xb6, 0x92, 0x0a, 0x17, 0x33, 0x8c, 0x14,
0x0f, 0x06, 0x3a, 0x7d, 0x10, 0x0b, 0x7f, 0x62, 0xe0, 0x4f, 0x64, 0xb6, 0x9e, 0x27, 0x52, 0x2d, 0x16, 0xc1, 0x81, 0x4e, 0xef, 0xc5, 0xc2, 0x9f, 0x18, 0xf8, 0x53, 0x99, 0xaf, 0xe6, 0xa9, 0x54,
0xdf, 0x4a, 0xb3, 0x4f, 0xc1, 0x5b, 0x60, 0x1c, 0x67, 0x81, 0x33, 0x74, 0x46, 0x83, 0x27, 0x8f, 0x8b, 0xb7, 0xd2, 0xec, 0x13, 0xf0, 0x22, 0x4c, 0x92, 0x3c, 0x70, 0x46, 0xce, 0xf8, 0xe0, 0xc9,
0x4e, 0xea, 0x66, 0xd4, 0x3c, 0x67, 0x18, 0xc7, 0xc2, 0xdc, 0xe2, 0x9f, 0x83, 0x9f, 0xe3, 0x26, 0xa3, 0xe3, 0xa6, 0x18, 0x0d, 0xcf, 0x29, 0x26, 0x89, 0x30, 0xbb, 0xf8, 0x67, 0xe0, 0x17, 0xb8,
0x8d, 0x65, 0x8e, 0x59, 0xe0, 0xea, 0x9f, 0xf0, 0xe6, 0x27, 0x57, 0x36, 0x25, 0x9a, 0x4b, 0xe1, 0xce, 0x12, 0x59, 0x60, 0x1e, 0xb8, 0xfa, 0x13, 0xde, 0x7e, 0x72, 0x65, 0x53, 0xa2, 0xdd, 0x14,
0x6f, 0x1d, 0x78, 0x70, 0x40, 0xc5, 0x8f, 0x80, 0xed, 0xf5, 0x57, 0x79, 0x82, 0xed, 0x09, 0x95, 0xfe, 0xd6, 0x83, 0x07, 0x7b, 0x54, 0xfc, 0x10, 0xd8, 0x4e, 0xdf, 0xca, 0x13, 0x6c, 0x47, 0xa8,
0xfa, 0x8b, 0x3c, 0xc1, 0x4a, 0x42, 0x3b, 0xdd, 0x3f, 0x4f, 0xb0, 0x1d, 0xa1, 0xb5, 0xee, 0x9a, 0xd2, 0x37, 0xf2, 0x04, 0xab, 0x08, 0x6d, 0x75, 0xfd, 0x3c, 0xc1, 0xb6, 0x84, 0x56, 0xba, 0x6a,
0x27, 0xd8, 0x9a, 0x7f, 0x0c, 0xbd, 0x1f, 0x0b, 0x54, 0x11, 0x66, 0x81, 0xa7, 0x2b, 0xff, 0xaf, 0x9e, 0x60, 0x2b, 0xfe, 0x11, 0x0c, 0x7e, 0x2c, 0x51, 0xc5, 0x98, 0x07, 0x9e, 0x3e, 0xf9, 0x7f,
0xa9, 0xfc, 0x5d, 0x81, 0xaa, 0x14, 0x55, 0x9e, 0x5e, 0xaa, 0x3b, 0x6e, 0xda, 0xa7, 0xcf, 0x14, 0xed, 0xc9, 0xdf, 0x96, 0xa8, 0x2a, 0x51, 0xe7, 0xe9, 0xa5, 0xba, 0xe2, 0xa6, 0x7c, 0x7a, 0x4d,
0xcb, 0xc9, 0x1d, 0x3d, 0x13, 0xa3, 0xb3, 0x55, 0xc8, 0xf4, 0x8c, 0x14, 0xfa, 0x02, 0x5c, 0xb9, 0xb1, 0x82, 0xdc, 0x31, 0x30, 0x31, 0x5a, 0x5b, 0x85, 0x4c, 0xcd, 0x48, 0xa1, 0xcf, 0xc1, 0x95,
0xc7, 0x2c, 0xf0, 0x35, 0xff, 0x07, 0x6f, 0x10, 0xe3, 0x64, 0xbc, 0xc7, 0xec, 0xeb, 0x6d, 0xae, 0x3b, 0xcc, 0x03, 0x5f, 0xf3, 0xbf, 0xff, 0x06, 0x31, 0x8e, 0x27, 0x3b, 0xcc, 0xbf, 0xda, 0x14,
0x4a, 0xa1, 0xaf, 0x3f, 0x7e, 0x06, 0x7e, 0x1d, 0x22, 0xe7, 0xbc, 0xc2, 0x52, 0x3f, 0xd0, 0x17, 0xaa, 0x12, 0x7a, 0xfb, 0xe3, 0x67, 0xe0, 0x37, 0x21, 0x72, 0xce, 0x2b, 0xac, 0xf4, 0x03, 0x7d,
0x74, 0xe4, 0x1f, 0x82, 0x77, 0x27, 0xe3, 0xc2, 0x08, 0x3f, 0x78, 0x72, 0xdc, 0xd0, 0x8e, 0xf7, 0x41, 0x4b, 0xfe, 0x01, 0x78, 0x77, 0x32, 0x29, 0x8d, 0xf0, 0x07, 0x4f, 0x8e, 0x5a, 0xda, 0xc9,
0x51, 0x26, 0x4c, 0xf2, 0xab, 0xce, 0x97, 0x2c, 0x7c, 0x1f, 0x5c, 0x0a, 0xf1, 0x77, 0xa0, 0x3b, 0x2e, 0xce, 0x85, 0x49, 0x7e, 0xd9, 0xfb, 0x82, 0x85, 0xdf, 0x83, 0x4b, 0x21, 0xaa, 0x75, 0x82,
0x4f, 0x8a, 0xed, 0x32, 0x0b, 0xd8, 0xd0, 0x19, 0x39, 0xc2, 0xa2, 0xf0, 0x4f, 0x46, 0x56, 0x33, 0x4b, 0x19, 0x55, 0x27, 0x69, 0xb9, 0x59, 0xe4, 0x01, 0x1b, 0x39, 0x63, 0x47, 0xec, 0xc5, 0xf8,
0xd2, 0xb6, 0xda, 0x6b, 0x3e, 0xfe, 0x5d, 0xe8, 0x93, 0xec, 0x2f, 0xef, 0xa4, 0xb2, 0x2d, 0xee, 0x3b, 0xd0, 0x9f, 0x9b, 0x6c, 0x6f, 0xe4, 0x8c, 0x7d, 0x61, 0x11, 0x7f, 0x08, 0x5e, 0x22, 0xe7,
0x11, 0xbe, 0x91, 0x8a, 0x7f, 0x06, 0x5d, 0x5d, 0xe4, 0x9e, 0x36, 0x57, 0x74, 0x37, 0x94, 0x17, 0x98, 0xd8, 0x36, 0x30, 0x20, 0xfc, 0x93, 0x91, 0x49, 0x4d, 0x51, 0x3a, 0xc6, 0x30, 0xcf, 0x7e,
0xf6, 0x5a, 0x2d, 0x96, 0xdb, 0x12, 0xeb, 0x21, 0x78, 0xb1, 0x9c, 0x63, 0x6c, 0x67, 0xc5, 0x00, 0x17, 0x86, 0x54, 0xb0, 0x97, 0x77, 0x52, 0x59, 0x73, 0x0c, 0x08, 0xdf, 0x48, 0xc5, 0x3f, 0x85,
0x32, 0x10, 0xa9, 0x5e, 0x6a, 0xad, 0xef, 0x65, 0x36, 0xbd, 0x31, 0xb7, 0xc2, 0x6b, 0x78, 0x70, 0xbe, 0xbe, 0xde, 0x3d, 0x06, 0xa9, 0xe9, 0x6e, 0x28, 0x2f, 0xec, 0xb6, 0x46, 0x66, 0xb7, 0x23,
0x50, 0xb1, 0xae, 0xc4, 0x0e, 0x2b, 0x35, 0x82, 0xf9, 0x56, 0x20, 0x1a, 0xb3, 0x0c, 0x63, 0x5c, 0x73, 0x73, 0x25, 0xaf, 0x73, 0x25, 0xb2, 0x1e, 0xd5, 0xab, 0xd2, 0x55, 0xba, 0x97, 0xd9, 0x54,
0xe4, 0xb8, 0xd4, 0x16, 0xe9, 0x8b, 0x1a, 0x87, 0xbf, 0xb0, 0x86, 0x57, 0xd7, 0xa3, 0x41, 0x5a, 0xd5, 0xec, 0x0a, 0xaf, 0xe1, 0xc1, 0xde, 0x89, 0xcd, 0x49, 0x6c, 0xff, 0xa4, 0x56, 0x6a, 0xdf,
0x24, 0x9b, 0x8d, 0xdc, 0x2e, 0x2d, 0x75, 0x05, 0x49, 0xb7, 0xe5, 0xdc, 0x52, 0x77, 0x96, 0x73, 0x4a, 0x4b, 0x0d, 0x9a, 0x63, 0x82, 0x51, 0x81, 0x0b, 0xad, 0xca, 0x50, 0x34, 0x38, 0xfc, 0x85,
0xc2, 0x2a, 0xb5, 0x4b, 0xa3, 0xa3, 0x52, 0x3e, 0x84, 0xc1, 0x06, 0x65, 0x56, 0x28, 0xdc, 0xe0, 0xb5, 0xbc, 0xfa, 0x3c, 0x6a, 0xc1, 0x28, 0x5d, 0xaf, 0xe5, 0x66, 0x61, 0xa9, 0x6b, 0x48, 0xba,
0x36, 0xb7, 0x12, 0xb4, 0x43, 0xfc, 0x11, 0xf4, 0x72, 0x79, 0xfb, 0x92, 0xda, 0x6c, 0xb4, 0xe8, 0x2d, 0xe6, 0x96, 0xba, 0xb7, 0x98, 0x13, 0x56, 0x99, 0xd5, 0xb9, 0xa7, 0x32, 0x3e, 0x82, 0x83,
0xe6, 0xf2, 0xf6, 0x1c, 0x4b, 0xfe, 0x1e, 0xf8, 0xab, 0x08, 0xe3, 0xa5, 0x4e, 0x19, 0xf3, 0xf5, 0x35, 0xca, 0xbc, 0x54, 0xb8, 0xc6, 0x4d, 0x61, 0x25, 0xe8, 0x86, 0xf8, 0x23, 0x18, 0x14, 0x72,
0x75, 0xe0, 0x1c, 0xcb, 0xf0, 0x57, 0x06, 0xdd, 0x19, 0xaa, 0x3b, 0x54, 0x6f, 0x35, 0x99, 0xed, 0xf9, 0x92, 0x0c, 0x62, 0xb4, 0xe8, 0x17, 0x72, 0x79, 0x8e, 0x15, 0x7f, 0x0f, 0xfc, 0xdb, 0x18,
0xcd, 0xe5, 0xfc, 0xcb, 0xe6, 0x72, 0xef, 0xdf, 0x5c, 0x5e, 0xb3, 0xb9, 0x1e, 0x82, 0x37, 0x53, 0x93, 0x85, 0x4e, 0x19, 0xdb, 0x0e, 0x75, 0xe0, 0x1c, 0xab, 0xf0, 0x57, 0x06, 0xfd, 0x19, 0xaa,
0x8b, 0xe9, 0x44, 0x7f, 0x91, 0x23, 0x0c, 0x20, 0x8f, 0x8d, 0x17, 0x79, 0x74, 0x87, 0x76, 0x9d, 0x3b, 0x54, 0x6f, 0xd5, 0xd3, 0xdd, 0x99, 0xe7, 0xfc, 0xcb, 0xcc, 0x73, 0xef, 0x9f, 0x79, 0x5e,
0x59, 0x14, 0xfe, 0xcc, 0xa0, 0x7b, 0x21, 0xcb, 0xa4, 0xc8, 0x5f, 0x73, 0xd8, 0x10, 0x06, 0xe3, 0x3b, 0xf3, 0x1e, 0x82, 0x37, 0x53, 0xd1, 0xd9, 0x54, 0xdf, 0xc8, 0x11, 0x06, 0x90, 0xf3, 0x26,
0x34, 0x8d, 0xa3, 0x85, 0xcc, 0xa3, 0x64, 0x6b, 0xbf, 0xb6, 0x1d, 0xa2, 0x1b, 0xcf, 0x5b, 0xda, 0x51, 0x11, 0xdf, 0xa1, 0x1d, 0x84, 0x16, 0x85, 0x3f, 0x33, 0xe8, 0x5f, 0xc8, 0x2a, 0x2d, 0x8b,
0x99, 0xef, 0x6e, 0x87, 0x68, 0x18, 0xce, 0xf4, 0xc2, 0x31, 0xdb, 0xa3, 0x35, 0x0c, 0x66, 0xcf, 0xd7, 0x1c, 0x36, 0x82, 0x83, 0x49, 0x96, 0x25, 0x71, 0x24, 0x8b, 0x38, 0xdd, 0xd8, 0xdb, 0x76,
0xe8, 0x24, 0x3d, 0x70, 0x5c, 0xe4, 0xc9, 0x2a, 0x4e, 0x76, 0xfa, 0x25, 0x7d, 0x51, 0xe3, 0xf0, 0x43, 0xb4, 0xe3, 0x79, 0x47, 0x3b, 0x73, 0xef, 0x6e, 0x88, 0xda, 0xe8, 0x54, 0x8f, 0x2a, 0x33,
0x2f, 0x06, 0xee, 0x7f, 0xb5, 0x48, 0x8e, 0x80, 0x45, 0xb6, 0x91, 0x2c, 0xaa, 0xd7, 0x4a, 0xaf, 0x77, 0x3a, 0x6d, 0x64, 0x26, 0x94, 0x4e, 0xd2, 0x03, 0x27, 0x65, 0x91, 0xde, 0x26, 0xe9, 0x56,
0xb5, 0x56, 0x02, 0xe8, 0x95, 0x4a, 0x6e, 0x6f, 0x31, 0x0b, 0xfa, 0x7a, 0x56, 0x2b, 0xa8, 0x33, 0xbf, 0x64, 0x28, 0x1a, 0x1c, 0xfe, 0xc5, 0xc0, 0xfd, 0xaf, 0x46, 0xd0, 0x21, 0xb0, 0xd8, 0x16,
0x7a, 0x46, 0xcc, 0x3e, 0xf1, 0x45, 0x05, 0x6b, 0xcf, 0x43, 0xe3, 0xf9, 0xf0, 0x0f, 0x06, 0x5e, 0x92, 0xc5, 0xcd, 0x40, 0x1a, 0x74, 0x06, 0x52, 0x00, 0x83, 0x4a, 0xc9, 0xcd, 0x12, 0xf3, 0x60,
0xed, 0xdc, 0xb3, 0x43, 0xe7, 0x9e, 0x35, 0xce, 0x9d, 0x9c, 0x56, 0xce, 0x9d, 0x9c, 0x12, 0x16, 0xa8, 0xfb, 0xbb, 0x86, 0x3a, 0xa3, 0x7b, 0xc4, 0x4c, 0x22, 0x5f, 0xd4, 0xb0, 0xf1, 0x3c, 0xb4,
0x97, 0x95, 0x73, 0xc5, 0x25, 0xa9, 0xf6, 0x4c, 0x25, 0x45, 0x7a, 0x5a, 0x1a, 0x79, 0x7d, 0x51, 0x9e, 0x0f, 0xff, 0x60, 0xe0, 0x35, 0xce, 0x3d, 0xdd, 0x77, 0xee, 0x69, 0xeb, 0xdc, 0xe9, 0x49,
0x63, 0x6a, 0xf7, 0xf7, 0x6b, 0x54, 0xf6, 0xcd, 0xbe, 0xb0, 0x88, 0xcc, 0x71, 0xa1, 0xa7, 0xda, 0xed, 0xdc, 0xe9, 0x09, 0x61, 0x71, 0x59, 0x3b, 0x57, 0x5c, 0x92, 0x6a, 0xcf, 0x54, 0x5a, 0x66,
0xbc, 0xd2, 0x00, 0xfe, 0x11, 0x78, 0x82, 0x5e, 0xa1, 0x9f, 0x7a, 0x20, 0x90, 0x0e, 0x0b, 0x93, 0x27, 0x95, 0x91, 0xd7, 0x17, 0x0d, 0xa6, 0x72, 0x7f, 0xb7, 0x42, 0x65, 0xdf, 0xec, 0x0b, 0x8b,
0x0d, 0x9f, 0xda, 0x6b, 0xc4, 0x72, 0x9d, 0xa6, 0xa8, 0xac, 0xa7, 0x0d, 0xd0, 0xdc, 0xc9, 0x0e, 0xc8, 0x1c, 0x17, 0xba, 0xab, 0xcd, 0x2b, 0x0d, 0xe0, 0x1f, 0x82, 0x27, 0xe8, 0x15, 0xfa, 0xa9,
0xcd, 0x3a, 0x72, 0x84, 0x01, 0xe1, 0x0f, 0xe0, 0x8f, 0x63, 0x54, 0xb9, 0x28, 0xe2, 0xd7, 0x97, 0x7b, 0x02, 0xe9, 0xb0, 0x30, 0xd9, 0xf0, 0xa9, 0xdd, 0x46, 0x2c, 0xd7, 0x59, 0x86, 0xca, 0x7a,
0x18, 0x07, 0xf7, 0x9b, 0xd9, 0xb7, 0x2f, 0xaa, 0x49, 0xa0, 0x73, 0xe3, 0x5f, 0xe7, 0x1f, 0xfe, 0xda, 0x00, 0xcd, 0x9d, 0x6e, 0xd1, 0x8c, 0x23, 0x47, 0x18, 0x10, 0xfe, 0x00, 0xfe, 0x24, 0x41,
0x3d, 0x97, 0xa9, 0x9c, 0x4e, 0x74, 0x63, 0x1d, 0x61, 0x51, 0xf8, 0x09, 0xb8, 0x34, 0x27, 0x2d, 0x55, 0x88, 0x32, 0x79, 0x7d, 0x88, 0x71, 0x70, 0xbf, 0x9e, 0x7d, 0xf3, 0xa2, 0xee, 0x04, 0x5a,
0x66, 0xf7, 0x4d, 0x33, 0x36, 0xef, 0xea, 0x7f, 0x1c, 0x4f, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xb7, 0xfe, 0x75, 0xfe, 0xe1, 0xdf, 0x73, 0x99, 0xc9, 0xb3, 0xa9, 0x2e, 0xac, 0x23, 0x2c, 0x0a,
0x94, 0xd8, 0xce, 0x85, 0x83, 0x08, 0x00, 0x00, 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 { 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 { message Template {

View File

@ -160,7 +160,8 @@ func Test_MarshalDashboard(t *testing.T) {
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"y": chronograf.Axis{ "y": chronograf.Axis{
Bounds: [2]int64{0, 100}, Bounds: []string{"0", "3", "1-7", "foo"},
Label: "foo",
}, },
}, },
Type: "line", 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)) 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 // Axis represents the visible extents of a visualization
type Axis struct { 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 // 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 ( import (
"fmt" "fmt"
"io" "io"
"testing"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
) )
@ -72,3 +73,11 @@ func (tl *TestLogger) stringifyArg(arg interface{}) []byte {
return []byte("UNKNOWN") 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" base := "/chronograf/v1/dashboards"
cells := make([]dashboardCellResponse, len(dcells)) cells := make([]dashboardCellResponse, len(dcells))
for i, cell := range dcells { for i, cell := range dcells {
if len(cell.Queries) == 0 { newCell := chronograf.DashboardCell{}
cell.Queries = make([]chronograf.DashboardQuery, 0)
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{ cells[i] = dashboardCellResponse{
DashboardCell: cell, DashboardCell: newCell,
Links: dashboardCellLinks{ Links: dashboardCellLinks{
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID), Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
}, },

View File

@ -1,9 +1,18 @@
package server_test package server_test
import ( import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing" "testing"
"github.com/bouk/httprouter"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/server" "github.com/influxdata/chronograf/server"
) )
@ -20,13 +29,13 @@ func Test_Cells_CorrectAxis(t *testing.T) {
&chronograf.DashboardCell{ &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: [2]int64{0, 100}, Bounds: []string{"0", "100"},
}, },
"y": chronograf.Axis{ "y": chronograf.Axis{
Bounds: [2]int64{0, 100}, Bounds: []string{"0", "100"},
}, },
"y2": chronograf.Axis{ "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{ &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"axis of evil": chronograf.Axis{ "axis of evil": chronograf.Axis{
Bounds: [2]int64{666, 666}, Bounds: []string{"666", "666"},
}, },
"axis of awesome": chronograf.Axis{ "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 { func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
base := "/chronograf/v1/dashboards" base := "/chronograf/v1/dashboards"
DashboardDefaults(&d) dd := AddQueryConfigs(DashboardDefaults(d))
AddQueryConfigs(&d) cells := newCellResponses(dd.ID, dd.Cells)
cells := newCellResponses(d.ID, d.Cells) templates := newTemplateResponses(dd.ID, dd.Templates)
templates := newTemplateResponses(d.ID, d.Templates)
return &dashboardResponse{ return &dashboardResponse{
ID: d.ID, ID: dd.ID,
Name: d.Name, Name: dd.Name,
Cells: cells, Cells: cells,
Templates: templates, Templates: templates,
Links: dashboardLinks{ Links: dashboardLinks{
Self: fmt.Sprintf("%s/%d", base, d.ID), Self: fmt.Sprintf("%s/%d", base, dd.ID),
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID), Cells: fmt.Sprintf("%s/%d/cells", base, dd.ID),
Templates: fmt.Sprintf("%s/%d/templates", base, d.ID), Templates: fmt.Sprintf("%s/%d/templates", base, dd.ID),
}, },
} }
} }
@ -229,24 +228,36 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error {
return err return err
} }
} }
DashboardDefaults(d) (*d) = DashboardDefaults(*d)
return nil return nil
} }
// DashboardDefaults updates the dashboard with the default values // DashboardDefaults updates the dashboard with the default values
// if none are specified // 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 { for i, c := range d.Cells {
CorrectWidthHeight(&c) CorrectWidthHeight(&c)
d.Cells[i] = c newDash.Cells[i] = c
} }
return
} }
// AddQueryConfigs updates all the celsl in the dashboard to have query config // AddQueryConfigs updates all the celsl in the dashboard to have query config
// objects corresponding to their influxql queries. // 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 { for i, c := range d.Cells {
AddQueryConfig(&c) 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 { 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) 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{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: [2]int64{0, 100}, Bounds: []string{"0", "100"},
}, },
"y": chronograf.Axis{ "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{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: [2]int64{0, 100}, Bounds: []string{"0", "100"},
}, },
"y": chronograf.Axis{ "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", ID: "b",
W: 4, W: 4,
H: 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{ Queries: []chronograf.DashboardQuery{
{ {
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", 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"` 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"` 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."` 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"` 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 ## 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'], 'spec/index.js': ['webpack', 'sourcemap'],
}, },
// For more detailed reporting on tests, you can add 'verbose' and/or 'progress'. // 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'], reporters: ['dots'],
webpack: { webpack: {
devtool: 'inline-source-map', devtool: 'inline-source-map',

View File

@ -1,6 +1,6 @@
{ {
"name": "chronograf-ui", "name": "chronograf-ui",
"version": "1.3.5-0", "version": "1.3.6-0",
"private": false, "private": false,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "", "description": "",
@ -9,13 +9,13 @@
"url": "github:influxdata/chronograf" "url": "github:influxdata/chronograf"
}, },
"scripts": { "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", "build:dev": "node_modules/webpack/bin/webpack.js --config ./webpack/devConfig.js",
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js", "start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
"lint": "node_modules/eslint/bin/eslint.js src/", "lint": "node_modules/eslint/bin/eslint.js src/",
"test": "karma start", "test": "karma start",
"test:lint": "npm run lint; npm run test", "test:lint": "yarn run lint; yarn run test",
"test:dev": "nodemon --exec npm run test:lint", "test:dev": "nodemon --exec yarn run test:lint",
"clean": "rm -rf build", "clean": "rm -rf build",
"storybook": "node ./storybook", "storybook": "node ./storybook",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix" "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, w: 4,
h: 4, h: 4,
id: 1, id: 1,
i: 'im-a-cell-id-index',
isEditing: false, isEditing: false,
name: 'Gigawatts', name: 'Gigawatts',
} }
@ -71,18 +72,6 @@ const editingCell = {
} }
const cells = [c1] 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', () => { describe('DataExplorer.Reducers.UI', () => {
it('can load the dashboards', () => { it('can load the dashboards', () => {

View File

@ -17,10 +17,18 @@ describe('getRangeForDygraphSpec', () => {
it('does not get range when a range is provided', () => { it('does not get range when a range is provided', () => {
const timeSeries = [[date, min], [date, max], [date, mid]] const timeSeries = [[date, min], [date, max], [date, mid]]
const providedRange = [0, 4] const providedRange = ['0', '4']
const actual = getRange(timeSeries, providedRange) 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', () => { it('gets the range for multiple timeSeries', () => {

View File

@ -1,7 +1,9 @@
import React, {Component, PropTypes} from 'react' import React, {Component, PropTypes} from 'react'
import _ from 'lodash' import _ from 'lodash'
import classnames from 'classnames' import classnames from 'classnames'
import {Link} from 'react-router' import {Link} from 'react-router'
import uuid from 'node-uuid'
import FancyScrollbar from 'shared/components/FancyScrollbar' import FancyScrollbar from 'shared/components/FancyScrollbar'
@ -130,10 +132,7 @@ class AlertsTable extends Component {
> >
{alerts.map(({name, level, time, host, value}) => { {alerts.map(({name, level, time, host, value}) => {
return ( return (
<div <div className="alert-history-table--tr" key={uuid.v4()}>
className="alert-history-table--tr"
key={`${name}-${level}-${time}-${host}-${value}`}
>
<div <div
className="alert-history-table--td" className="alert-history-table--td"
style={{width: colName}} 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 QueryMaker from 'src/data_explorer/components/QueryMaker'
import Visualization from 'src/data_explorer/components/Visualization' import Visualization from 'src/data_explorer/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls' import OverlayControls from 'src/dashboards/components/OverlayControls'
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
import * as queryModifiers from 'src/utils/queryTransitions' import * as queryModifiers from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import buildInfluxQLQuery from 'utils/influxql' import buildInfluxQLQuery from 'utils/influxql'
import {getQueryConfig} from 'shared/apis' import {getQueryConfig} from 'shared/apis'
import {buildYLabel} from 'shared/presenters'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants' import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames' import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
@ -22,17 +25,17 @@ class CellEditorOverlay extends Component {
super(props) super(props)
this.queryStateReducer = ::this.queryStateReducer this.queryStateReducer = ::this.queryStateReducer
this.handleAddQuery = ::this.handleAddQuery this.handleAddQuery = ::this.handleAddQuery
this.handleDeleteQuery = ::this.handleDeleteQuery this.handleDeleteQuery = ::this.handleDeleteQuery
this.handleSaveCell = ::this.handleSaveCell this.handleSaveCell = ::this.handleSaveCell
this.handleSelectGraphType = ::this.handleSelectGraphType this.handleSelectGraphType = ::this.handleSelectGraphType
this.handleClickDisplayOptionsTab = ::this.handleClickDisplayOptionsTab
this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
this.handleEditRawText = ::this.handleEditRawText 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( const queriesWorkingDraft = _.cloneDeep(
queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})) queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()}))
@ -43,9 +46,26 @@ class CellEditorOverlay extends Component {
cellWorkingType: type, cellWorkingType: type,
queriesWorkingDraft, queriesWorkingDraft,
activeQueryIndex: 0, 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) { componentWillReceiveProps(nextProps) {
const {status, queryID} = this.props.queryStatus const {status, queryID} = this.props.queryStatus
const nextStatus = nextProps.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) { handleAddQuery(options) {
const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options) const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
const nextQueries = this.state.queriesWorkingDraft.concat(newQuery) const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
@ -87,31 +125,44 @@ class CellEditorOverlay extends Component {
} }
handleSaveCell() { handleSaveCell() {
const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state const {
queriesWorkingDraft,
cellWorkingType: type,
cellWorkingName: name,
axes,
} = this.state
const {cell} = this.props const {cell} = this.props
const newCell = _.cloneDeep(cell) const queries = queriesWorkingDraft.map(q => {
newCell.name = cellWorkingName
newCell.type = cellWorkingType
newCell.queries = queriesWorkingDraft.map(q => {
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
const query = q.rawText || buildInfluxQLQuery(timeRange, q) const query = q.rawText || buildInfluxQLQuery(timeRange, q)
const label = q.rawText ? '' : `${q.measurement}.${q.fields[0].field}`
return { return {
queryConfig: q, queryConfig: q,
query, query,
label,
} }
}) })
this.props.onSave(newCell) this.props.onSave({
...cell,
name,
type,
queries,
axes,
})
} }
handleSelectGraphType(graphType) { handleSelectGraphType(graphType) {
this.setState({cellWorkingType: graphType}) this.setState({cellWorkingType: graphType})
} }
handleClickDisplayOptionsTab(isDisplayOptionsTabActive) {
return () => {
this.setState({isDisplayOptionsTabActive})
}
}
handleSetActiveQueryIndex(activeQueryIndex) { handleSetActiveQueryIndex(activeQueryIndex) {
this.setState({activeQueryIndex}) this.setState({activeQueryIndex})
} }
@ -146,7 +197,9 @@ class CellEditorOverlay extends Component {
activeQueryIndex, activeQueryIndex,
cellWorkingName, cellWorkingName,
cellWorkingType, cellWorkingType,
isDisplayOptionsTabActive,
queriesWorkingDraft, queriesWorkingDraft,
axes,
} = this.state } = this.state
const queryActions = { const queryActions = {
@ -177,27 +230,36 @@ class CellEditorOverlay extends Component {
cellType={cellWorkingType} cellType={cellWorkingType}
cellName={cellWorkingName} cellName={cellWorkingName}
editQueryStatus={editQueryStatus} editQueryStatus={editQueryStatus}
axes={axes}
views={[]} views={[]}
/> />
<div className="overlay-technology--editor"> <div className="overlay-technology--editor">
<OverlayControls <OverlayControls
selectedGraphType={cellWorkingType} isDisplayOptionsTabActive={isDisplayOptionsTabActive}
onSelectGraphType={this.handleSelectGraphType} onClickDisplayOptions={this.handleClickDisplayOptionsTab}
onCancel={onCancel} onCancel={onCancel}
onSave={this.handleSaveCell} onSave={this.handleSaveCell}
isSavable={queriesWorkingDraft.every(isQuerySavable)} isSavable={queriesWorkingDraft.every(isQuerySavable)}
/> />
<QueryMaker {isDisplayOptionsTabActive
source={source} ? <DisplayOptions
templates={templates} selectedGraphType={cellWorkingType}
queries={queriesWorkingDraft} onSelectGraphType={this.handleSelectGraphType}
actions={queryActions} onSetRange={this.handleSetYAxisBounds}
autoRefresh={autoRefresh} onSetLabel={this.handleSetLabel}
timeRange={timeRange} axes={axes}
setActiveQueryIndex={this.handleSetActiveQueryIndex} />
onDeleteQuery={this.handleDeleteQuery} : <QueryMaker
activeQueryIndex={activeQueryIndex} source={source}
/> templates={templates}
queries={queriesWorkingDraft}
actions={queryActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
activeQueryIndex={activeQueryIndex}
/>}
</div> </div>
</ResizeContainer> </ResizeContainer>
</div> </div>
@ -232,6 +294,7 @@ CellEditorOverlay.propTypes = {
queryID: string, queryID: string,
status: shape({}), status: shape({}),
}).isRequired, }).isRequired,
dashboardID: string.isRequired,
} }
export default CellEditorOverlay 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 ConfirmButtons from 'shared/components/ConfirmButtons'
import graphTypes from 'hson!shared/data/graphTypes.hson' const OverlayControls = ({
onCancel,
const OverlayControls = props => { onSave,
const { isDisplayOptionsTabActive,
onCancel, onClickDisplayOptions,
onSave, isSavable,
selectedGraphType, }) =>
onSelectGraphType, <div className="overlay-controls">
isSavable, <h3 className="overlay--graph-name">Cell Editor</h3>
} = props <ul className="nav nav-tablist nav-tablist-sm">
return ( <li
<div className="overlay-controls"> key="queries"
<h3 className="overlay--graph-name">Cell Editor</h3> className={classnames({
<div className="overlay-controls--right"> active: !isDisplayOptionsTabActive,
<p>Visualization Type:</p> })}
<ul className="nav nav-tablist nav-tablist-sm"> onClick={onClickDisplayOptions(false)}
{graphTypes.map(graphType => >
<li Queries
key={graphType.type} </li>
className={classnames({ <li
active: graphType.type === selectedGraphType, key="displayOptions"
})} className={classnames({
onClick={() => onSelectGraphType(graphType.type)} active: isDisplayOptionsTabActive,
> })}
{graphType.menuOption} onClick={onClickDisplayOptions(true)}
</li> >
)} Display Options
</ul> </li>
<ConfirmButtons </ul>
onCancel={onCancel} <div className="overlay-controls--right">
onConfirm={onSave} <ConfirmButtons
isDisabled={!isSavable} onCancel={onCancel}
/> onConfirm={onSave}
</div> isDisabled={!isSavable}
/>
</div> </div>
) </div>
}
const {func, string, bool} = PropTypes const {func, bool} = PropTypes
OverlayControls.propTypes = { OverlayControls.propTypes = {
onCancel: func.isRequired, onCancel: func.isRequired,
onSave: func.isRequired, onSave: func.isRequired,
selectedGraphType: string.isRequired, isDisplayOptionsTabActive: bool.isRequired,
onSelectGraphType: func.isRequired, onClickDisplayOptions: func.isRequired,
isSavable: bool, isSavable: bool,
} }

View File

@ -209,7 +209,11 @@ class DashboardPage extends Component {
const dygraphs = [...this.state.dygraphs, dygraph] const dygraphs = [...this.state.dygraphs, dygraph]
const {dashboards, params} = this.props const {dashboards, params} = this.props
const dashboard = dashboards.find(d => d.id === +params.dashboardID) 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, { Dygraph.synchronize(dygraphs, {
selection: true, selection: true,
zoom: false, zoom: false,
@ -248,7 +252,7 @@ class DashboardPage extends Component {
inPresentationMode, inPresentationMode,
handleChooseAutoRefresh, handleChooseAutoRefresh,
handleClickPresentationButton, handleClickPresentationButton,
params: {sourceID}, params: {sourceID, dashboardID},
} = this.props } = this.props
const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant' const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant'
@ -322,6 +326,7 @@ class DashboardPage extends Component {
{selectedCell {selectedCell
? <CellEditorOverlay ? <CellEditorOverlay
source={source} source={source}
dashboardID={dashboardID}
templates={templatesIncludingDashTime} templates={templatesIncludingDashTime}
cell={selectedCell} cell={selectedCell}
timeRange={timeRange} timeRange={timeRange}
@ -372,8 +377,8 @@ class DashboardPage extends Component {
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
synchronizer={this.synchronizer} synchronizer={this.synchronizer}
onAddCell={this.handleAddCell} onAddCell={this.handleAddCell}
inPresentationMode={inPresentationMode}
onEditCell={this.handleEditDashboardCell} onEditCell={this.handleEditDashboardCell}
inPresentationMode={inPresentationMode}
onPositionChange={this.handleUpdatePosition} onPositionChange={this.handleUpdatePosition}
onDeleteCell={this.handleDeleteDashboardCell} onDeleteCell={this.handleDeleteDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell} 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 {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes
const defaultTableHeight = 1000
const emptySeries = {columns: [], values: []} const emptySeries = {columns: [], values: []}
const CustomCell = React.createClass({ const CustomCell = React.createClass({
@ -64,7 +63,7 @@ const ChronoTable = React.createClass({
getDefaultProps() { getDefaultProps() {
return { return {
height: defaultTableHeight, height: 500,
} }
}, },
@ -139,11 +138,11 @@ const ChronoTable = React.createClass({
const maximumTabsCount = 11 const maximumTabsCount = 11
// adjust height to proper value by subtracting the heights of the UI around it // 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 // 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 minWidth = 70
const rowHeight = 34
const headerHeight = 30
const stylePixelOffset = 125
const defaultColumnWidth = 200
const styleAdjustedHeight = height - stylePixelOffset const styleAdjustedHeight = height - stylePixelOffset
const width = const width =
columns && columns.length > 1 ? defaultColumnWidth : containerWidth columns && columns.length > 1 ? defaultColumnWidth : containerWidth

View File

@ -1,13 +1,10 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import Table from './Table' import Table from './Table'
import AutoRefresh from 'shared/components/AutoRefresh' import RefreshingGraph from 'shared/components/RefreshingGraph'
import LineGraph from 'shared/components/LineGraph'
import SingleStat from 'shared/components/SingleStat'
const RefreshingLineGraph = AutoRefresh(LineGraph)
const RefreshingSingleStat = AutoRefresh(SingleStat)
const VisView = ({ const VisView = ({
axes,
view, view,
queries, queries,
cellType, cellType,
@ -16,7 +13,7 @@ const VisView = ({
heightPixels, heightPixels,
editQueryStatus, editQueryStatus,
activeQueryIndex, activeQueryIndex,
isInDataExplorer, resizerBottomHeight,
}) => { }) => {
const activeQuery = queries[activeQueryIndex] const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0] const defaultQuery = queries[0]
@ -34,46 +31,30 @@ const VisView = ({
return ( return (
<Table <Table
query={query} query={query}
height={heightPixels} height={resizerBottomHeight}
editQueryStatus={editQueryStatus} 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 ( return (
<RefreshingLineGraph <RefreshingGraph
axes={axes}
type={cellType}
queries={queries} queries={queries}
autoRefresh={autoRefresh}
templates={templates} templates={templates}
activeQueryIndex={activeQueryIndex} cellHeight={heightPixels}
isInDataExplorer={isInDataExplorer} autoRefresh={autoRefresh}
showSingleStat={cellType === 'line-plus-single-stat'}
isBarGraph={cellType === 'bar'}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus} editQueryStatus={editQueryStatus}
/> />
) )
} }
const {arrayOf, bool, func, number, shape, string} = PropTypes const {arrayOf, func, number, shape, string} = PropTypes
VisView.propTypes = { VisView.propTypes = {
view: string.isRequired, view: string.isRequired,
axes: shape(),
queries: arrayOf(shape()).isRequired, queries: arrayOf(shape()).isRequired,
cellType: string, cellType: string,
templates: arrayOf(shape()), templates: arrayOf(shape()),
@ -81,7 +62,7 @@ VisView.propTypes = {
heightPixels: number, heightPixels: number,
editQueryStatus: func.isRequired, editQueryStatus: func.isRequired,
activeQueryIndex: number, activeQueryIndex: number,
isInDataExplorer: bool, resizerBottomHeight: number,
} }
export default VisView export default VisView

View File

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

View File

@ -33,7 +33,7 @@ const WriteDataBody = ({
ref={fileInput} ref={fileInput}
accept="text/*, application/gzip" 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'} {uploadContent ? 'Upload a Different File' : 'Upload a File'}
</button> </button>
{uploadContent {uploadContent

View File

@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants' import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
const { const {
array,
arrayOf, arrayOf,
bool, bool,
element, element,
@ -43,6 +44,12 @@ const AutoRefresh = ComposedComponent => {
text: string, text: string,
}).isRequired }).isRequired
).isRequired, ).isRequired,
axes: shape({
bounds: shape({
y: array,
y2: array,
}),
}),
editQueryStatus: func, editQueryStatus: func,
}, },

View File

@ -9,6 +9,13 @@ import getRange from 'shared/parsing/getRangeForDygraph'
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers' import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
import DygraphLegend from 'src/shared/components/DygraphLegend' 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 { export default class Dygraph extends Component {
constructor(props) { constructor(props) {
@ -35,12 +42,14 @@ export default class Dygraph extends Component {
this.handleHideLegend = ::this.handleHideLegend this.handleHideLegend = ::this.handleHideLegend
this.handleToggleFilter = ::this.handleToggleFilter this.handleToggleFilter = ::this.handleToggleFilter
this.visibility = ::this.visibility this.visibility = ::this.visibility
this.getLabel = ::this.getLabel
} }
static defaultProps = { static defaultProps = {
containerStyle: {}, containerStyle: {},
isGraphFilled: true, isGraphFilled: true,
overrideLineColors: null, overrideLineColors: null,
dygraphRef: () => {},
} }
getTimeSeries() { getTimeSeries() {
@ -50,11 +59,23 @@ export default class Dygraph extends Component {
return timeSeries.length ? timeSeries : [[0]] 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() { componentDidMount() {
const timeSeries = this.getTimeSeries() const timeSeries = this.getTimeSeries()
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'}; // dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
const { const {
ranges, axes,
dygraphSeries, dygraphSeries,
ruleValues, ruleValues,
overrideLineColors, overrideLineColors,
@ -65,18 +86,29 @@ export default class Dygraph extends Component {
const graphRef = this.graphRef const graphRef = this.graphRef
const legendRef = this.legendRef const legendRef = this.legendRef
let finalLineColors = overrideLineColors const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
if (finalLineColors === null) { const hashColorDygraphSeries = {}
finalLineColors = LINE_COLORS 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 = { const defaultOptions = {
plugins: [ plugins: isBarGraph
new Dygraphs.Plugins.Crosshair({ ? []
direction: 'vertical', : [
}), new Dygraphs.Plugins.Crosshair({
], direction: 'vertical',
}),
],
labelsSeparateLines: false, labelsSeparateLines: false,
labelsKMB: true, labelsKMB: true,
rightGap: 0, rightGap: 0,
@ -85,22 +117,22 @@ export default class Dygraph extends Component {
fillGraph: isGraphFilled, fillGraph: isGraphFilled,
axisLineWidth: 2, axisLineWidth: 2,
gridLineWidth: 1, gridLineWidth: 1,
highlightCircleSize: 3, highlightCircleSize: isBarGraph ? 0 : 3,
animatedZooms: true, animatedZooms: true,
hideOverlayOnMouseOut: false, hideOverlayOnMouseOut: false,
colors: finalLineColors, colors: finalLineColors,
series: dygraphSeries, series: hashColorDygraphSeries,
axes: { axes: {
y: { y: {
valueRange: getRange(timeSeries, ranges.y, ruleValues), valueRange: getRange(timeSeries, yAxis, ruleValues),
}, },
y2: { y2: {
valueRange: getRange(timeSeries, ranges.y2), valueRange: getRange(timeSeries, y2Axis),
}, },
}, },
highlightSeriesOpts: { highlightSeriesOpts: {
strokeWidth: 2, strokeWidth: 2,
highlightCircleSize: 5, highlightCircleSize: isBarGraph ? 0 : 5,
}, },
legendFormatter: legend => { legendFormatter: legend => {
if (!legend.x) { if (!legend.x) {
@ -235,11 +267,12 @@ export default class Dygraph extends Component {
componentDidUpdate() { componentDidUpdate() {
const { const {
labels, labels,
ranges, axes,
options, options,
dygraphSeries, dygraphSeries,
ruleValues, ruleValues,
isBarGraph, isBarGraph,
overrideLineColors,
} = this.props } = this.props
const dygraph = this.dygraph 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 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 = { const updateOptions = {
labels, labels,
file: timeSeries, file: timeSeries,
ylabel,
axes: { axes: {
y: { y: {
valueRange: getRange(timeSeries, ranges.y, ruleValues), valueRange: getRange(timeSeries, y, ruleValues),
}, },
y2: { y2: {
valueRange: getRange(timeSeries, ranges.y2), valueRange: getRange(timeSeries, y2),
}, },
}, },
stepPlot: options.stepPlot, stepPlot: options.stepPlot,
stackedGraph: options.stackedGraph, stackedGraph: options.stackedGraph,
underlayCallback: options.underlayCallback, underlayCallback: options.underlayCallback,
series: dygraphSeries, colors: finalLineColors,
series: hashColorDygraphSeries,
plotter: isBarGraph ? multiColumnBarPlotter : null, plotter: isBarGraph ? multiColumnBarPlotter : null,
visibility: this.visibility(), visibility: this.visibility(),
} }
@ -350,6 +400,7 @@ export default class Dygraph extends Component {
<div <div
ref={r => { ref={r => {
this.graphRef = r this.graphRef = r
this.props.dygraphRef(r)
}} }}
style={this.props.containerStyle} style={this.props.containerStyle}
className="dygraph-child-container" 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 = { Dygraph.propTypes = {
ranges: shape({ axes: shape({
y: arrayOf(number), y: shape({
y2: arrayOf(number), bounds: array,
}),
y2: shape({
bounds: array,
}),
}), }),
queries: arrayOf(shape),
timeSeries: array.isRequired, timeSeries: array.isRequired,
labels: array.isRequired, labels: array.isRequired,
options: shape({}), options: shape({}),
@ -384,4 +440,5 @@ Dygraph.propTypes = {
}), }),
synchronizer: func, synchronizer: func,
setResolution: func, setResolution: func,
dygraphRef: func,
} }

View File

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

View File

@ -2,7 +2,6 @@ import React, {PropTypes} from 'react'
import Dygraph from 'shared/components/Dygraph' import Dygraph from 'shared/components/Dygraph'
import classnames from 'classnames' import classnames from 'classnames'
import shallowCompare from 'react-addons-shallow-compare' import shallowCompare from 'react-addons-shallow-compare'
import _ from 'lodash'
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph' import timeSeriesToDygraph from 'utils/timeSeriesToDygraph'
import lastValues from 'shared/parsing/lastValues' import lastValues from 'shared/parsing/lastValues'
@ -15,9 +14,15 @@ export default React.createClass({
displayName: 'LineGraph', displayName: 'LineGraph',
propTypes: { propTypes: {
data: arrayOf(shape({}).isRequired).isRequired, data: arrayOf(shape({}).isRequired).isRequired,
ranges: shape({ axes: shape({
y: arrayOf(number), y: shape({
y2: arrayOf(number), bounds: array,
label: string,
}),
y2: shape({
bounds: array,
label: string,
}),
}), }),
title: string, title: string,
isFetchingInitially: bool, isFetchingInitially: bool,
@ -81,15 +86,15 @@ export default React.createClass({
render() { render() {
const { const {
data, data,
ranges, axes,
isFetchingInitially, isFetchingInitially,
isRefreshing, isRefreshing,
isGraphFilled, isGraphFilled,
isBarGraph, isBarGraph,
overrideLineColors, overrideLineColors,
title, title,
underlayCallback,
queries, queries,
underlayCallback,
showSingleStat, showSingleStat,
displayOptions, displayOptions,
ruleValues, ruleValues,
@ -120,8 +125,6 @@ export default React.createClass({
axisLabelWidth: 60, axisLabelWidth: 60,
drawAxesAtZero: true, drawAxesAtZero: true,
underlayCallback, underlayCallback,
ylabel: _.get(queries, ['0', 'label'], ''),
y2label: _.get(queries, ['1', 'label'], ''),
...displayOptions, ...displayOptions,
} }
@ -151,26 +154,25 @@ export default React.createClass({
roundedValue = Math.round(+lastValue * precision) / precision roundedValue = Math.round(+lastValue * precision) / precision
} }
const lineColors = showSingleStat
? singleStatLineColors
: overrideLineColors
return ( return (
<div <div className={`dygraph ${this.yLabelClass()}`} style={{height: '100%'}}>
className={classnames('dygraph', {
'graph--hasYLabel': !!(options.ylabel || options.y2label),
})}
style={{height: '100%'}}
>
{isRefreshing ? this.renderSpinner() : null} {isRefreshing ? this.renderSpinner() : null}
<Dygraph <Dygraph
axes={axes}
queries={queries}
dygraphRef={this.dygraphRefFunc}
containerStyle={{width: '100%', height: '100%'}} containerStyle={{width: '100%', height: '100%'}}
overrideLineColors={ overrideLineColors={lineColors}
showSingleStat ? singleStatLineColors : overrideLineColors
}
isGraphFilled={showSingleStat ? false : isGraphFilled} isGraphFilled={showSingleStat ? false : isGraphFilled}
isBarGraph={isBarGraph} isBarGraph={isBarGraph}
timeSeries={timeSeries} timeSeries={timeSeries}
labels={labels} labels={labels}
options={showSingleStat ? singleStatOptions : options} options={showSingleStat ? singleStatOptions : options}
dygraphSeries={dygraphSeries} dygraphSeries={dygraphSeries}
ranges={ranges || this.getRanges()}
ruleValues={ruleValues} ruleValues={ruleValues}
synchronizer={synchronizer} synchronizer={synchronizer}
timeRange={timeRange} 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() { renderSpinner() {
return ( return (
<div className="graph-panel__refreshing"> <div className="graph-panel__refreshing">
@ -202,25 +224,4 @@ export default React.createClass({
</div> </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 RefreshingSingleStat = AutoRefresh(SingleStat)
const RefreshingGraph = ({ const RefreshingGraph = ({
timeRange, axes,
autoRefresh,
templates,
synchronizer,
type, type,
queries, queries,
templates,
timeRange,
cellHeight, cellHeight,
autoRefresh,
synchronizer,
editQueryStatus,
}) => { }) => {
if (type === 'single-stat') { if (type === 'single-stat') {
return ( return (
@ -42,6 +44,8 @@ const RefreshingGraph = ({
isBarGraph={type === 'bar'} isBarGraph={type === 'bar'}
displayOptions={displayOptions} displayOptions={displayOptions}
synchronizer={synchronizer} synchronizer={synchronizer}
editQueryStatus={editQueryStatus}
axes={axes}
/> />
) )
} }
@ -56,8 +60,10 @@ RefreshingGraph.propTypes = {
templates: arrayOf(shape()), templates: arrayOf(shape()),
synchronizer: func, synchronizer: func,
type: string.isRequired, type: string.isRequired,
cellHeight: number,
axes: shape(),
queries: arrayOf(shape()).isRequired, queries: arrayOf(shape()).isRequired,
cellHeight: number.isRequired, editQueryStatus: func,
} }
export default RefreshingGraph export default RefreshingGraph

View File

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

View File

@ -1,8 +1,8 @@
[ [
{type: "line", menuOption: "Line"}, {type: "line", menuOption: "Line", graphic: "Wogglez"},
{type: "line-stacked", menuOption: "Stacked"}, {type: "line-stacked", menuOption: "Stacked", graphic: "Rogglez"},
{type: "line-stepplot", menuOption: "Step-Plot"}, {type: "line-stepplot", menuOption: "Step-Plot", graphic: "Fogglez"},
{type: "single-stat", menuOption: "SingleStat"}, {type: "single-stat", menuOption: "SingleStat", graphic: "Bogglez"},
{type: "line-plus-single-stat", menuOption: "Line + Stat"}, {type: "line-plus-single-stat", menuOption: "Line + Stat", graphic: "Togglez"},
{type: "bar", menuOption: "Bar"}, {type: "bar", menuOption: "Bar", graphic: "Zogglez"},
] ]

View File

@ -26,7 +26,7 @@ export const darkenColor = colorStr => {
return `rgb(${color.r},${color.g},${color.b})` 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 => { export const multiColumnBarPlotter = e => {
// We need to handle all the series simultaneously. // We need to handle all the series simultaneously.
if (e.seriesIndex !== 0) { 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 fillColors = []
const strokeColors = g.getColors() 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++) { for (let i = 0; i < strokeColors.length; i++) {
fillColors.push(darkenColor(strokeColors[i])) fillColors.push(darkenColor(strokeColors[i]))
} }
ctx.lineWidth = 2
for (let j = 0; j < sets.length; j++) { for (let j = 0; j < sets.length; j++) {
ctx.fillStyle = fillColors[j]
ctx.strokeStyle = strokeColors[j] ctx.strokeStyle = strokeColors[j]
for (let i = 0; i < sets[j].length; i++) { for (let i = 0; i < sets[j].length; i++) {
const p = sets[j][i] const p = sets[j][i]
const centerX = p.canvasx const centerX = p.canvasx
ctx.fillStyle = fillColors[j]
const xLeft = const xLeft =
sets.length === 1 sets.length === 1
? centerX - barWidth / 2 ? centerX - barWidth
: centerX - barWidth / 2 * (1 - j / (sets.length - 1)) : centerX - barWidth * (1 - j / sets.length)
ctx.fillRect( ctx.fillRect(
xLeft, xLeft,
@ -77,12 +87,15 @@ export const multiColumnBarPlotter = e => {
yBottom - p.canvasy yBottom - p.canvasy
) )
ctx.strokeRect( // hover highlighting
xLeft, if (selPointX === centerX) {
p.canvasy, ctx.strokeRect(
barWidth / sets.length, xLeft,
yBottom - p.canvasy p.canvasy,
) barWidth / sets.length,
yBottom - p.canvasy
)
}
} }
} }
} }

View File

@ -1,15 +1,24 @@
const PADDING_FACTOR = 0.1 const PADDING_FACTOR = 0.1
export default function getRange( const considerEmpty = (userNumber, number) => {
timeSeries, if (userNumber === '') {
override, return null
ruleValues = {value: null, rangeValue: null}
) {
if (override) {
return override
} }
if (userNumber) {
return +userNumber
}
return number
}
const getRange = (
timeSeries,
userSelectedRange = [null, null],
ruleValues = {value: null, rangeValue: null}
) => {
const {value, rangeValue, operator} = ruleValues const {value, rangeValue, operator} = ruleValues
const [userMin, userMax] = userSelectedRange
const subtractPadding = val => +val - Math.abs(val * PADDING_FACTOR) const subtractPadding = val => +val - Math.abs(val * PADDING_FACTOR)
const addPadding = 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] [null, null]
) )
const [min, max] = range
// If time series is such that min and max are equal use Dygraph defaults // 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 [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) 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 * as actionTypes from 'src/status/constants/actionTypes'
import {HTTP_NOT_FOUND} from 'shared/constants'
const fetchJSONFeedRequested = () => ({ const fetchJSONFeedRequested = () => ({
type: actionTypes.FETCH_JSON_FEED_REQUESTED, type: actionTypes.FETCH_JSON_FEED_REQUESTED,
}) })
@ -45,15 +43,11 @@ export const fetchJSONFeedAsync = url => async dispatch => {
} catch (error) { } catch (error) {
console.error(error) console.error(error)
dispatch(fetchJSONFeedFailed()) dispatch(fetchJSONFeedFailed())
if (error.status === HTTP_NOT_FOUND) { dispatch(
dispatch( errorThrown(
errorThrown( error,
error, `Failed to fetch JSON Feed for News Feed from '${url}'`
`Failed to fetch News Feed. JSON Feed at '${url}' returned 404 (Not Found)`
)
) )
} else { )
dispatch(errorThrown(error, 'Failed to fetch NewsFeed'))
}
} }
} }

View File

@ -25,6 +25,7 @@
@import 'layout/flash-messages'; @import 'layout/flash-messages';
// Components // Components
@import 'components/ceo-display-options';
@import 'components/confirm-buttons'; @import 'components/confirm-buttons';
@import 'components/custom-time-range'; @import 'components/custom-time-range';
@import 'components/dygraphs'; @import 'components/dygraphs';
@ -47,7 +48,6 @@
@import 'components/tables'; @import 'components/tables';
// Pages // Pages
@import 'pages/config-endpoints'; @import 'pages/config-endpoints';
@import 'pages/signup'; @import 'pages/signup';
@import 'pages/auth-page'; @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--max-width: 960px;
$write-data--gutter: 30px; $write-data--gutter: 30px;
$write-data--margin: 18px; $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--transition: opacity 0.4s ease;
.write-data-form { .write-data-form {

View File

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