Merge branch 'master' into fix/ie11-support
commit
eb0ab9523b
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -1,12 +1,33 @@
|
|||
## v1.3.6.0 [unreleased]
|
||||
## v1.3.7.0 [unreleased]
|
||||
### Bug Fixes
|
||||
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
|
||||
1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix no-scroll bar appearing in the Data Explorer table
|
||||
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix missing cell type (and consequently single-stat)
|
||||
|
||||
### Features
|
||||
### UI Improvements
|
||||
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written
|
||||
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
|
||||
|
||||
## v1.3.5.0 [2017-07-25]
|
||||
### UI Improvements
|
||||
1. [#1846](https://github.com/influxdata/chronograf/pull/1846): Increase screen real estate of Query Maker in the Cell Editor Overlay
|
||||
|
||||
## v1.3.6.0 [2017-08-08]
|
||||
### Bug Fixes
|
||||
1. [#1798](https://github.com/influxdata/chronograf/pull/1798): Fix domain not updating in visualizations when changing time range manually
|
||||
1. [#1799](https://github.com/influxdata/chronograf/pull/1799): Prevent console error spam from Dygraph's synchronize method when a dashboard has only one graph
|
||||
1. [#1813](https://github.com/influxdata/chronograf/pull/1813): Guarantee UUID for each Alert Table key to prevent dropping items when keys overlap
|
||||
|
||||
### Features
|
||||
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis bounds
|
||||
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis label
|
||||
|
||||
### UI Improvements
|
||||
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner write data modal to indicate data is being written
|
||||
1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Fix bar graphs overlapping
|
||||
1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Assign a series consistent coloring when it appears in multiple cells
|
||||
1. [#1800](https://github.com/influxdata/chronograf/pull/1800): Increase size of line protocol manual entry in Data Explorer's Write Data overlay
|
||||
1. [#1812](https://github.com/influxdata/chronograf/pull/1812): Improve error message when request for Status Page News Feed fails
|
||||
|
||||
## v1.3.5.0 [2017-07-27]
|
||||
### Bug Fixes
|
||||
1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu
|
||||
1. [#1752](https://github.com/influxdata/chronograf/pull/1752): Clarify BoltPath server flag help text by making example the default path
|
||||
|
|
|
@ -21,7 +21,7 @@ We really like to receive feature requests, as it helps us prioritize our work.
|
|||
|
||||
Contributing to the source code
|
||||
-------------------------------
|
||||
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses NPM for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
|
||||
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses Yarn for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
|
||||
|
||||
Submitting a pull request
|
||||
-------------------------
|
||||
|
@ -43,9 +43,9 @@ Signing the CLA
|
|||
If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found
|
||||
[on our website](https://influxdata.com/community/cla/).
|
||||
|
||||
Installing NPM
|
||||
Installing Yarn
|
||||
--------------
|
||||
You'll need to install NPM to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the NPM downloads page](https://nodejs.org/en/download/).
|
||||
You'll need to install Yarn to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the Yarn installation page](https://yarnpkg.com/en/docs/install).
|
||||
|
||||
Installing Go
|
||||
-------------
|
||||
|
@ -105,7 +105,7 @@ Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessa
|
|||
|
||||
Build and Test
|
||||
--------------
|
||||
Make sure you have `go` and `npm` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following:
|
||||
Make sure you have `go` and `yarn` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following:
|
||||
|
||||
```bash
|
||||
cd $GOPATH/src/github.com/influxdata/chronograf
|
||||
|
|
8
Makefile
8
Makefile
|
@ -60,11 +60,11 @@ canned/bin_gen.go: canned/*.json
|
|||
go generate -x ./canned
|
||||
|
||||
.jssrc: $(UISOURCES)
|
||||
cd ui && npm run build
|
||||
cd ui && yarn run build
|
||||
@touch .jssrc
|
||||
|
||||
.dev-jssrc: $(UISOURCES)
|
||||
cd ui && npm run build:dev
|
||||
cd ui && yarn run build:dev
|
||||
@touch .dev-jssrc
|
||||
|
||||
dep: .jsdep .godep
|
||||
|
@ -98,7 +98,7 @@ gotestrace:
|
|||
go test -race `go list ./... | grep -v /vendor/`
|
||||
|
||||
jstest:
|
||||
cd ui && npm test
|
||||
cd ui && yarn test
|
||||
|
||||
run: ${BINARY}
|
||||
./chronograf
|
||||
|
@ -108,7 +108,7 @@ run-dev: chronogiraffe
|
|||
|
||||
clean:
|
||||
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
|
||||
cd ui && npm run clean
|
||||
cd ui && yarn run clean
|
||||
cd ui && rm -rf node_modules
|
||||
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
|
||||
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata
|
||||
|
|
|
@ -183,14 +183,9 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
|
||||
axes := make(map[string]*Axis, len(c.Axes))
|
||||
for a, r := range c.Axes {
|
||||
// need to explicitly allocate a new array because r.Bounds is
|
||||
// over-written and the resulting slices from previous iterations will
|
||||
// point to later iteration's data. It is _not_ enough to simply re-slice
|
||||
// r.Bounds
|
||||
axis := [2]int64{}
|
||||
copy(axis[:], r.Bounds[:2])
|
||||
axes[a] = &Axis{
|
||||
Bounds: axis[:],
|
||||
Bounds: r.Bounds,
|
||||
Label: r.Label,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,9 +264,16 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
|
||||
axes := make(map[string]chronograf.Axis, len(c.Axes))
|
||||
for a, r := range c.Axes {
|
||||
axis := chronograf.Axis{}
|
||||
copy(axis.Bounds[:], r.Bounds[:2])
|
||||
axes[a] = axis
|
||||
if r.Bounds != nil {
|
||||
axes[a] = chronograf.Axis{
|
||||
Bounds: r.Bounds,
|
||||
Label: r.Label,
|
||||
}
|
||||
} else {
|
||||
axes[a] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells[i] = chronograf.DashboardCell{
|
||||
|
|
|
@ -118,7 +118,9 @@ func (m *DashboardCell) GetAxes() map[string]*Axis {
|
|||
}
|
||||
|
||||
type Axis struct {
|
||||
Bounds []int64 `protobuf:"varint,1,rep,name=bounds" json:"bounds,omitempty"`
|
||||
LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"`
|
||||
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
|
||||
Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Axis) Reset() { *m = Axis{} }
|
||||
|
@ -313,65 +315,66 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 952 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4,
|
||||
0x13, 0x56, 0xc7, 0x76, 0x12, 0x57, 0x66, 0xe7, 0xf7, 0x53, 0x6b, 0xc5, 0x9a, 0x45, 0x42, 0xc1,
|
||||
0x02, 0x29, 0x20, 0x31, 0xa0, 0x5d, 0x21, 0x21, 0x6e, 0x99, 0x09, 0x5a, 0x85, 0x99, 0x5d, 0x86,
|
||||
0xce, 0xcc, 0x70, 0x42, 0xab, 0x4e, 0x52, 0x99, 0x58, 0xeb, 0xc4, 0xa6, 0x6d, 0x4f, 0xe2, 0xb7,
|
||||
0xe0, 0x09, 0x90, 0x90, 0x38, 0x71, 0xe0, 0xc0, 0x0b, 0xf0, 0x10, 0xbc, 0x10, 0xaa, 0xee, 0xf6,
|
||||
0x9f, 0xb0, 0xb3, 0x68, 0x4f, 0xdc, 0xfa, 0xab, 0xea, 0x7c, 0xe5, 0xfe, 0xea, 0xab, 0x52, 0xe0,
|
||||
0x38, 0xda, 0xe6, 0xa8, 0xb6, 0x32, 0x3e, 0x49, 0x55, 0x92, 0x27, 0xbc, 0x5f, 0xe1, 0xf0, 0xf7,
|
||||
0x0e, 0x74, 0x67, 0x49, 0xa1, 0x16, 0xc8, 0x8f, 0xa1, 0x33, 0x9d, 0x04, 0x6c, 0xc8, 0x46, 0x8e,
|
||||
0xe8, 0x4c, 0x27, 0x9c, 0x83, 0xfb, 0x42, 0x6e, 0x30, 0xe8, 0x0c, 0xd9, 0xc8, 0x17, 0xfa, 0x4c,
|
||||
0xb1, 0xab, 0x32, 0xc5, 0xc0, 0x31, 0x31, 0x3a, 0xf3, 0xc7, 0xd0, 0xbf, 0xce, 0x88, 0x6d, 0x83,
|
||||
0x81, 0xab, 0xe3, 0x35, 0xa6, 0xdc, 0xa5, 0xcc, 0xb2, 0x5d, 0xa2, 0x96, 0x81, 0x67, 0x72, 0x15,
|
||||
0xe6, 0xff, 0x07, 0xe7, 0x5a, 0x5c, 0x04, 0x5d, 0x1d, 0xa6, 0x23, 0x0f, 0xa0, 0x37, 0xc1, 0x95,
|
||||
0x2c, 0xe2, 0x3c, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x2a, 0x48, 0x3c, 0x57, 0x18, 0xe3, 0xad, 0x92,
|
||||
0xab, 0xa0, 0x6f, 0x78, 0x2a, 0xcc, 0x4f, 0x80, 0x4f, 0xb7, 0x19, 0x2e, 0x0a, 0x85, 0xb3, 0x57,
|
||||
0x51, 0x7a, 0x83, 0x2a, 0x5a, 0x95, 0x81, 0xaf, 0x09, 0xee, 0xc9, 0x50, 0x95, 0xe7, 0x98, 0x4b,
|
||||
0xaa, 0x0d, 0x9a, 0xaa, 0x82, 0x3c, 0x84, 0xa3, 0xd9, 0x5a, 0x2a, 0x5c, 0xce, 0x70, 0xa1, 0x30,
|
||||
0x0f, 0x06, 0x3a, 0x7d, 0x10, 0x0b, 0x7f, 0x62, 0xe0, 0x4f, 0x64, 0xb6, 0x9e, 0x27, 0x52, 0x2d,
|
||||
0xdf, 0x4a, 0xb3, 0x4f, 0xc1, 0x5b, 0x60, 0x1c, 0x67, 0x81, 0x33, 0x74, 0x46, 0x83, 0x27, 0x8f,
|
||||
0x4e, 0xea, 0x66, 0xd4, 0x3c, 0x67, 0x18, 0xc7, 0xc2, 0xdc, 0xe2, 0x9f, 0x83, 0x9f, 0xe3, 0x26,
|
||||
0x8d, 0x65, 0x8e, 0x59, 0xe0, 0xea, 0x9f, 0xf0, 0xe6, 0x27, 0x57, 0x36, 0x25, 0x9a, 0x4b, 0xe1,
|
||||
0x6f, 0x1d, 0x78, 0x70, 0x40, 0xc5, 0x8f, 0x80, 0xed, 0xf5, 0x57, 0x79, 0x82, 0xed, 0x09, 0x95,
|
||||
0xfa, 0x8b, 0x3c, 0xc1, 0x4a, 0x42, 0x3b, 0xdd, 0x3f, 0x4f, 0xb0, 0x1d, 0xa1, 0xb5, 0xee, 0x9a,
|
||||
0x27, 0xd8, 0x9a, 0x7f, 0x0c, 0xbd, 0x1f, 0x0b, 0x54, 0x11, 0x66, 0x81, 0xa7, 0x2b, 0xff, 0xaf,
|
||||
0xa9, 0xfc, 0x5d, 0x81, 0xaa, 0x14, 0x55, 0x9e, 0x5e, 0xaa, 0x3b, 0x6e, 0xda, 0xa7, 0xcf, 0x14,
|
||||
0xcb, 0xc9, 0x1d, 0x3d, 0x13, 0xa3, 0xb3, 0x55, 0xc8, 0xf4, 0x8c, 0x14, 0xfa, 0x02, 0x5c, 0xb9,
|
||||
0xc7, 0x2c, 0xf0, 0x35, 0xff, 0x07, 0x6f, 0x10, 0xe3, 0x64, 0xbc, 0xc7, 0xec, 0xeb, 0x6d, 0xae,
|
||||
0x4a, 0xa1, 0xaf, 0x3f, 0x7e, 0x06, 0x7e, 0x1d, 0x22, 0xe7, 0xbc, 0xc2, 0x52, 0x3f, 0xd0, 0x17,
|
||||
0x74, 0xe4, 0x1f, 0x82, 0x77, 0x27, 0xe3, 0xc2, 0x08, 0x3f, 0x78, 0x72, 0xdc, 0xd0, 0x8e, 0xf7,
|
||||
0x51, 0x26, 0x4c, 0xf2, 0xab, 0xce, 0x97, 0x2c, 0x7c, 0x1f, 0x5c, 0x0a, 0xf1, 0x77, 0xa0, 0x3b,
|
||||
0x4f, 0x8a, 0xed, 0x32, 0x0b, 0xd8, 0xd0, 0x19, 0x39, 0xc2, 0xa2, 0xf0, 0x4f, 0x46, 0x56, 0x33,
|
||||
0xd2, 0xb6, 0xda, 0x6b, 0x3e, 0xfe, 0x5d, 0xe8, 0x93, 0xec, 0x2f, 0xef, 0xa4, 0xb2, 0x2d, 0xee,
|
||||
0x11, 0xbe, 0x91, 0x8a, 0x7f, 0x06, 0x5d, 0x5d, 0xe4, 0x9e, 0x36, 0x57, 0x74, 0x37, 0x94, 0x17,
|
||||
0xf6, 0x5a, 0x2d, 0x96, 0xdb, 0x12, 0xeb, 0x21, 0x78, 0xb1, 0x9c, 0x63, 0x6c, 0x67, 0xc5, 0x00,
|
||||
0x32, 0x10, 0xa9, 0x5e, 0x6a, 0xad, 0xef, 0x65, 0x36, 0xbd, 0x31, 0xb7, 0xc2, 0x6b, 0x78, 0x70,
|
||||
0x50, 0xb1, 0xae, 0xc4, 0x0e, 0x2b, 0x35, 0x82, 0xf9, 0x56, 0x20, 0x1a, 0xb3, 0x0c, 0x63, 0x5c,
|
||||
0xe4, 0xb8, 0xd4, 0x16, 0xe9, 0x8b, 0x1a, 0x87, 0xbf, 0xb0, 0x86, 0x57, 0xd7, 0xa3, 0x41, 0x5a,
|
||||
0x24, 0x9b, 0x8d, 0xdc, 0x2e, 0x2d, 0x75, 0x05, 0x49, 0xb7, 0xe5, 0xdc, 0x52, 0x77, 0x96, 0x73,
|
||||
0xc2, 0x2a, 0xb5, 0x4b, 0xa3, 0xa3, 0x52, 0x3e, 0x84, 0xc1, 0x06, 0x65, 0x56, 0x28, 0xdc, 0xe0,
|
||||
0x36, 0xb7, 0x12, 0xb4, 0x43, 0xfc, 0x11, 0xf4, 0x72, 0x79, 0xfb, 0x92, 0xda, 0x6c, 0xb4, 0xe8,
|
||||
0xe6, 0xf2, 0xf6, 0x1c, 0x4b, 0xfe, 0x1e, 0xf8, 0xab, 0x08, 0xe3, 0xa5, 0x4e, 0x19, 0xf3, 0xf5,
|
||||
0x75, 0xe0, 0x1c, 0xcb, 0xf0, 0x57, 0x06, 0xdd, 0x19, 0xaa, 0x3b, 0x54, 0x6f, 0x35, 0x99, 0xed,
|
||||
0xcd, 0xe5, 0xfc, 0xcb, 0xe6, 0x72, 0xef, 0xdf, 0x5c, 0x5e, 0xb3, 0xb9, 0x1e, 0x82, 0x37, 0x53,
|
||||
0x8b, 0xe9, 0x44, 0x7f, 0x91, 0x23, 0x0c, 0x20, 0x8f, 0x8d, 0x17, 0x79, 0x74, 0x87, 0x76, 0x9d,
|
||||
0x59, 0x14, 0xfe, 0xcc, 0xa0, 0x7b, 0x21, 0xcb, 0xa4, 0xc8, 0x5f, 0x73, 0xd8, 0x10, 0x06, 0xe3,
|
||||
0x34, 0x8d, 0xa3, 0x85, 0xcc, 0xa3, 0x64, 0x6b, 0xbf, 0xb6, 0x1d, 0xa2, 0x1b, 0xcf, 0x5b, 0xda,
|
||||
0x99, 0xef, 0x6e, 0x87, 0x68, 0x18, 0xce, 0xf4, 0xc2, 0x31, 0xdb, 0xa3, 0x35, 0x0c, 0x66, 0xcf,
|
||||
0xe8, 0x24, 0x3d, 0x70, 0x5c, 0xe4, 0xc9, 0x2a, 0x4e, 0x76, 0xfa, 0x25, 0x7d, 0x51, 0xe3, 0xf0,
|
||||
0x2f, 0x06, 0xee, 0x7f, 0xb5, 0x48, 0x8e, 0x80, 0x45, 0xb6, 0x91, 0x2c, 0xaa, 0xd7, 0x4a, 0xaf,
|
||||
0xb5, 0x56, 0x02, 0xe8, 0x95, 0x4a, 0x6e, 0x6f, 0x31, 0x0b, 0xfa, 0x7a, 0x56, 0x2b, 0xa8, 0x33,
|
||||
0x7a, 0x46, 0xcc, 0x3e, 0xf1, 0x45, 0x05, 0x6b, 0xcf, 0x43, 0xe3, 0xf9, 0xf0, 0x0f, 0x06, 0x5e,
|
||||
0xed, 0xdc, 0xb3, 0x43, 0xe7, 0x9e, 0x35, 0xce, 0x9d, 0x9c, 0x56, 0xce, 0x9d, 0x9c, 0x12, 0x16,
|
||||
0x97, 0x95, 0x73, 0xc5, 0x25, 0xa9, 0xf6, 0x4c, 0x25, 0x45, 0x7a, 0x5a, 0x1a, 0x79, 0x7d, 0x51,
|
||||
0x63, 0x6a, 0xf7, 0xf7, 0x6b, 0x54, 0xf6, 0xcd, 0xbe, 0xb0, 0x88, 0xcc, 0x71, 0xa1, 0xa7, 0xda,
|
||||
0xbc, 0xd2, 0x00, 0xfe, 0x11, 0x78, 0x82, 0x5e, 0xa1, 0x9f, 0x7a, 0x20, 0x90, 0x0e, 0x0b, 0x93,
|
||||
0x0d, 0x9f, 0xda, 0x6b, 0xc4, 0x72, 0x9d, 0xa6, 0xa8, 0xac, 0xa7, 0x0d, 0xd0, 0xdc, 0xc9, 0x0e,
|
||||
0xcd, 0x3a, 0x72, 0x84, 0x01, 0xe1, 0x0f, 0xe0, 0x8f, 0x63, 0x54, 0xb9, 0x28, 0xe2, 0xd7, 0x97,
|
||||
0x18, 0x07, 0xf7, 0x9b, 0xd9, 0xb7, 0x2f, 0xaa, 0x49, 0xa0, 0x73, 0xe3, 0x5f, 0xe7, 0x1f, 0xfe,
|
||||
0x3d, 0x97, 0xa9, 0x9c, 0x4e, 0x74, 0x63, 0x1d, 0x61, 0x51, 0xf8, 0x09, 0xb8, 0x34, 0x27, 0x2d,
|
||||
0x66, 0xf7, 0x4d, 0x33, 0x36, 0xef, 0xea, 0x7f, 0x1c, 0x4f, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff,
|
||||
0x94, 0xd8, 0xce, 0x85, 0x83, 0x08, 0x00, 0x00,
|
||||
// 974 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xdf, 0x6e, 0xe3, 0xc4,
|
||||
0x17, 0xd6, 0xc4, 0x76, 0x12, 0x9f, 0x76, 0xfb, 0xfb, 0x69, 0xb4, 0x62, 0xcd, 0x72, 0x13, 0x2c,
|
||||
0x90, 0x02, 0x12, 0x05, 0xed, 0x0a, 0x09, 0x71, 0x97, 0x36, 0x68, 0x55, 0xda, 0x5d, 0xca, 0xa4,
|
||||
0x2d, 0xdc, 0xa0, 0xd5, 0xc4, 0x39, 0x4d, 0xac, 0x75, 0x62, 0x33, 0xb6, 0x9b, 0xf8, 0x2d, 0x78,
|
||||
0x02, 0x24, 0x24, 0xae, 0xb8, 0xe0, 0x82, 0x17, 0xe0, 0x21, 0x78, 0x21, 0x74, 0x66, 0xc6, 0x7f,
|
||||
0xc2, 0x76, 0xd1, 0x5e, 0x71, 0x37, 0xdf, 0x39, 0xe3, 0x6f, 0x66, 0xbe, 0xf3, 0x9d, 0x23, 0xc3,
|
||||
0x51, 0xbc, 0x29, 0x50, 0x6d, 0x64, 0x72, 0x9c, 0xa9, 0xb4, 0x48, 0xf9, 0xb0, 0xc6, 0xe1, 0xef,
|
||||
0x3d, 0xe8, 0xcf, 0xd2, 0x52, 0x45, 0xc8, 0x8f, 0xa0, 0x77, 0x36, 0x0d, 0xd8, 0x88, 0x8d, 0x1d,
|
||||
0xd1, 0x3b, 0x9b, 0x72, 0x0e, 0xee, 0x0b, 0xb9, 0xc6, 0xa0, 0x37, 0x62, 0x63, 0x5f, 0xe8, 0x35,
|
||||
0xc5, 0xae, 0xaa, 0x0c, 0x03, 0xc7, 0xc4, 0x68, 0xcd, 0x1f, 0xc3, 0xf0, 0x3a, 0x27, 0xb6, 0x35,
|
||||
0x06, 0xae, 0x8e, 0x37, 0x98, 0x72, 0x97, 0x32, 0xcf, 0xb7, 0xa9, 0x5a, 0x04, 0x9e, 0xc9, 0xd5,
|
||||
0x98, 0xff, 0x1f, 0x9c, 0x6b, 0x71, 0x11, 0xf4, 0x75, 0x98, 0x96, 0x3c, 0x80, 0xc1, 0x14, 0x6f,
|
||||
0x65, 0x99, 0x14, 0xc1, 0x60, 0xc4, 0xc6, 0x43, 0x51, 0x43, 0xe2, 0xb9, 0xc2, 0x04, 0x97, 0x4a,
|
||||
0xde, 0x06, 0x43, 0xc3, 0x53, 0x63, 0x7e, 0x0c, 0xfc, 0x6c, 0x93, 0x63, 0x54, 0x2a, 0x9c, 0xbd,
|
||||
0x8a, 0xb3, 0x1b, 0x54, 0xf1, 0x6d, 0x15, 0xf8, 0x9a, 0xe0, 0x9e, 0x0c, 0x9d, 0xf2, 0x1c, 0x0b,
|
||||
0x49, 0x67, 0x83, 0xa6, 0xaa, 0x21, 0x0f, 0xe1, 0x70, 0xb6, 0x92, 0x0a, 0x17, 0x33, 0x8c, 0x14,
|
||||
0x16, 0xc1, 0x81, 0x4e, 0xef, 0xc5, 0xc2, 0x9f, 0x18, 0xf8, 0x53, 0x99, 0xaf, 0xe6, 0xa9, 0x54,
|
||||
0x8b, 0xb7, 0xd2, 0xec, 0x13, 0xf0, 0x22, 0x4c, 0x92, 0x3c, 0x70, 0x46, 0xce, 0xf8, 0xe0, 0xc9,
|
||||
0xa3, 0xe3, 0xa6, 0x18, 0x0d, 0xcf, 0x29, 0x26, 0x89, 0x30, 0xbb, 0xf8, 0x67, 0xe0, 0x17, 0xb8,
|
||||
0xce, 0x12, 0x59, 0x60, 0x1e, 0xb8, 0xfa, 0x13, 0xde, 0x7e, 0x72, 0x65, 0x53, 0xa2, 0xdd, 0x14,
|
||||
0xfe, 0xd6, 0x83, 0x07, 0x7b, 0x54, 0xfc, 0x10, 0xd8, 0x4e, 0xdf, 0xca, 0x13, 0x6c, 0x47, 0xa8,
|
||||
0xd2, 0x37, 0xf2, 0x04, 0xab, 0x08, 0x6d, 0x75, 0xfd, 0x3c, 0xc1, 0xb6, 0x84, 0x56, 0xba, 0x6a,
|
||||
0x9e, 0x60, 0x2b, 0xfe, 0x11, 0x0c, 0x7e, 0x2c, 0x51, 0xc5, 0x98, 0x07, 0x9e, 0x3e, 0xf9, 0x7f,
|
||||
0xed, 0xc9, 0xdf, 0x96, 0xa8, 0x2a, 0x51, 0xe7, 0xe9, 0xa5, 0xba, 0xe2, 0xa6, 0x7c, 0x7a, 0x4d,
|
||||
0xb1, 0x82, 0xdc, 0x31, 0x30, 0x31, 0x5a, 0x5b, 0x85, 0x4c, 0xcd, 0x48, 0xa1, 0xcf, 0xc1, 0x95,
|
||||
0x3b, 0xcc, 0x03, 0x5f, 0xf3, 0xbf, 0xff, 0x06, 0x31, 0x8e, 0x27, 0x3b, 0xcc, 0xbf, 0xda, 0x14,
|
||||
0xaa, 0x12, 0x7a, 0xfb, 0xe3, 0x67, 0xe0, 0x37, 0x21, 0x72, 0xce, 0x2b, 0xac, 0xf4, 0x03, 0x7d,
|
||||
0x41, 0x4b, 0xfe, 0x01, 0x78, 0x77, 0x32, 0x29, 0x8d, 0xf0, 0x07, 0x4f, 0x8e, 0x5a, 0xda, 0xc9,
|
||||
0x2e, 0xce, 0x85, 0x49, 0x7e, 0xd9, 0xfb, 0x82, 0x85, 0xdf, 0x83, 0x4b, 0x21, 0xaa, 0x75, 0x82,
|
||||
0x4b, 0x19, 0x55, 0x27, 0x69, 0xb9, 0x59, 0xe4, 0x01, 0x1b, 0x39, 0x63, 0x47, 0xec, 0xc5, 0xf8,
|
||||
0x3b, 0xd0, 0x9f, 0x9b, 0x6c, 0x6f, 0xe4, 0x8c, 0x7d, 0x61, 0x11, 0x7f, 0x08, 0x5e, 0x22, 0xe7,
|
||||
0x98, 0xd8, 0x36, 0x30, 0x20, 0xfc, 0x93, 0x91, 0x49, 0x4d, 0x51, 0x3a, 0xc6, 0x30, 0xcf, 0x7e,
|
||||
0x17, 0x86, 0x54, 0xb0, 0x97, 0x77, 0x52, 0x59, 0x73, 0x0c, 0x08, 0xdf, 0x48, 0xc5, 0x3f, 0x85,
|
||||
0xbe, 0xbe, 0xde, 0x3d, 0x06, 0xa9, 0xe9, 0x6e, 0x28, 0x2f, 0xec, 0xb6, 0x46, 0x66, 0xb7, 0x23,
|
||||
0x73, 0x73, 0x25, 0xaf, 0x73, 0x25, 0xb2, 0x1e, 0xd5, 0xab, 0xd2, 0x55, 0xba, 0x97, 0xd9, 0x54,
|
||||
0xd5, 0xec, 0x0a, 0xaf, 0xe1, 0xc1, 0xde, 0x89, 0xcd, 0x49, 0x6c, 0xff, 0xa4, 0x56, 0x6a, 0xdf,
|
||||
0x4a, 0x4b, 0x0d, 0x9a, 0x63, 0x82, 0x51, 0x81, 0x0b, 0xad, 0xca, 0x50, 0x34, 0x38, 0xfc, 0x85,
|
||||
0xb5, 0xbc, 0xfa, 0x3c, 0x6a, 0xc1, 0x28, 0x5d, 0xaf, 0xe5, 0x66, 0x61, 0xa9, 0x6b, 0x48, 0xba,
|
||||
0x2d, 0xe6, 0x96, 0xba, 0xb7, 0x98, 0x13, 0x56, 0x99, 0xd5, 0xb9, 0xa7, 0x32, 0x3e, 0x82, 0x83,
|
||||
0x35, 0xca, 0xbc, 0x54, 0xb8, 0xc6, 0x4d, 0x61, 0x25, 0xe8, 0x86, 0xf8, 0x23, 0x18, 0x14, 0x72,
|
||||
0xf9, 0x92, 0x0c, 0x62, 0xb4, 0xe8, 0x17, 0x72, 0x79, 0x8e, 0x15, 0x7f, 0x0f, 0xfc, 0xdb, 0x18,
|
||||
0x93, 0x85, 0x4e, 0x19, 0xdb, 0x0e, 0x75, 0xe0, 0x1c, 0xab, 0xf0, 0x57, 0x06, 0xfd, 0x19, 0xaa,
|
||||
0x3b, 0x54, 0x6f, 0xd5, 0xd3, 0xdd, 0x99, 0xe7, 0xfc, 0xcb, 0xcc, 0x73, 0xef, 0x9f, 0x79, 0x5e,
|
||||
0x3b, 0xf3, 0x1e, 0x82, 0x37, 0x53, 0xd1, 0xd9, 0x54, 0xdf, 0xc8, 0x11, 0x06, 0x90, 0xf3, 0x26,
|
||||
0x51, 0x11, 0xdf, 0xa1, 0x1d, 0x84, 0x16, 0x85, 0x3f, 0x33, 0xe8, 0x5f, 0xc8, 0x2a, 0x2d, 0x8b,
|
||||
0xd7, 0x1c, 0x36, 0x82, 0x83, 0x49, 0x96, 0x25, 0x71, 0x24, 0x8b, 0x38, 0xdd, 0xd8, 0xdb, 0x76,
|
||||
0x43, 0xb4, 0xe3, 0x79, 0x47, 0x3b, 0x73, 0xef, 0x6e, 0x88, 0xda, 0xe8, 0x54, 0x8f, 0x2a, 0x33,
|
||||
0x77, 0x3a, 0x6d, 0x64, 0x26, 0x94, 0x4e, 0xd2, 0x03, 0x27, 0x65, 0x91, 0xde, 0x26, 0xe9, 0x56,
|
||||
0xbf, 0x64, 0x28, 0x1a, 0x1c, 0xfe, 0xc5, 0xc0, 0xfd, 0xaf, 0x46, 0xd0, 0x21, 0xb0, 0xd8, 0x16,
|
||||
0x92, 0xc5, 0xcd, 0x40, 0x1a, 0x74, 0x06, 0x52, 0x00, 0x83, 0x4a, 0xc9, 0xcd, 0x12, 0xf3, 0x60,
|
||||
0xa8, 0xfb, 0xbb, 0x86, 0x3a, 0xa3, 0x7b, 0xc4, 0x4c, 0x22, 0x5f, 0xd4, 0xb0, 0xf1, 0x3c, 0xb4,
|
||||
0x9e, 0x0f, 0xff, 0x60, 0xe0, 0x35, 0xce, 0x3d, 0xdd, 0x77, 0xee, 0x69, 0xeb, 0xdc, 0xe9, 0x49,
|
||||
0xed, 0xdc, 0xe9, 0x09, 0x61, 0x71, 0x59, 0x3b, 0x57, 0x5c, 0x92, 0x6a, 0xcf, 0x54, 0x5a, 0x66,
|
||||
0x27, 0x95, 0x91, 0xd7, 0x17, 0x0d, 0xa6, 0x72, 0x7f, 0xb7, 0x42, 0x65, 0xdf, 0xec, 0x0b, 0x8b,
|
||||
0xc8, 0x1c, 0x17, 0xba, 0xab, 0xcd, 0x2b, 0x0d, 0xe0, 0x1f, 0x82, 0x27, 0xe8, 0x15, 0xfa, 0xa9,
|
||||
0x7b, 0x02, 0xe9, 0xb0, 0x30, 0xd9, 0xf0, 0xa9, 0xdd, 0x46, 0x2c, 0xd7, 0x59, 0x86, 0xca, 0x7a,
|
||||
0xda, 0x00, 0xcd, 0x9d, 0x6e, 0xd1, 0x8c, 0x23, 0x47, 0x18, 0x10, 0xfe, 0x00, 0xfe, 0x24, 0x41,
|
||||
0x55, 0x88, 0x32, 0x79, 0x7d, 0x88, 0x71, 0x70, 0xbf, 0x9e, 0x7d, 0xf3, 0xa2, 0xee, 0x04, 0x5a,
|
||||
0xb7, 0xfe, 0x75, 0xfe, 0xe1, 0xdf, 0x73, 0x99, 0xc9, 0xb3, 0xa9, 0x2e, 0xac, 0x23, 0x2c, 0x0a,
|
||||
0x3f, 0x06, 0x97, 0xfa, 0xa4, 0xc3, 0xec, 0xbe, 0xa9, 0xc7, 0xe6, 0x7d, 0xfd, 0xaf, 0xf2, 0xf4,
|
||||
0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x79, 0x79, 0x87, 0x37, 0xbd, 0x08, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ message DashboardCell {
|
|||
}
|
||||
|
||||
message Axis {
|
||||
repeated int64 bounds = 1; // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds.
|
||||
string label = 3; // label is a description of this axis
|
||||
}
|
||||
|
||||
message Template {
|
||||
|
|
|
@ -160,7 +160,8 @@ func Test_MarshalDashboard(t *testing.T) {
|
|||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "3", "1-7", "foo"},
|
||||
Label: "foo",
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
|
@ -179,3 +180,149 @@ func Test_MarshalDashboard(t *testing.T) {
|
|||
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(dashboard, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
||||
dashboard := chronograf.Dashboard{
|
||||
ID: 1,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Super awesome query",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "select * from cpu",
|
||||
Label: "CPU Utilization",
|
||||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
LegacyBounds: [2]int64{0, 5},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "Dashboard",
|
||||
}
|
||||
|
||||
expected := chronograf.Dashboard{
|
||||
ID: 1,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Super awesome query",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "select * from cpu",
|
||||
Label: "CPU Utilization",
|
||||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "Dashboard",
|
||||
}
|
||||
|
||||
var actual chronograf.Dashboard
|
||||
if buf, err := internal.MarshalDashboard(dashboard); err != nil {
|
||||
t.Fatal("Error marshaling dashboard: err", err)
|
||||
} else if err := internal.UnmarshalDashboard(buf, &actual); err != nil {
|
||||
t.Fatal("Error unmarshaling dashboard: err:", err)
|
||||
} else if !cmp.Equal(expected, actual) {
|
||||
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) {
|
||||
dashboard := chronograf.Dashboard{
|
||||
ID: 1,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Super awesome query",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "select * from cpu",
|
||||
Label: "CPU Utilization",
|
||||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
LegacyBounds: [2]int64{},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "Dashboard",
|
||||
}
|
||||
|
||||
expected := chronograf.Dashboard{
|
||||
ID: 1,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Super awesome query",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "select * from cpu",
|
||||
Label: "CPU Utilization",
|
||||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "Dashboard",
|
||||
}
|
||||
|
||||
var actual chronograf.Dashboard
|
||||
if buf, err := internal.MarshalDashboard(dashboard); err != nil {
|
||||
t.Fatal("Error marshaling dashboard: err", err)
|
||||
} else if err := internal.UnmarshalDashboard(buf, &actual); err != nil {
|
||||
t.Fatal("Error unmarshaling dashboard: err:", err)
|
||||
} else if !cmp.Equal(expected, actual) {
|
||||
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -569,7 +569,9 @@ type Dashboard struct {
|
|||
|
||||
// Axis represents the visible extents of a visualization
|
||||
type Axis struct {
|
||||
Bounds [2]int64 `json:"bounds"` // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
Bounds []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a cell
|
||||
LegacyBounds [2]int64 `json:"-"` // legacy bounds are for testing a migration from an earlier version of axis
|
||||
Label string `json:"label"` // label is a description of this Axis
|
||||
}
|
||||
|
||||
// DashboardCell holds visual and query information for a cell
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -3,6 +3,7 @@ package mocks
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
@ -72,3 +73,11 @@ func (tl *TestLogger) stringifyArg(arg interface{}) []byte {
|
|||
return []byte("UNKNOWN")
|
||||
}
|
||||
}
|
||||
|
||||
// Dump dumps out logs into a given testing.T's logs
|
||||
func (tl *TestLogger) Dump(t *testing.T) {
|
||||
t.Log("== Dumping Test Logs ==")
|
||||
for _, msg := range tl.Messages {
|
||||
t.Logf("lvl: %s, msg: %s", msg.Level, msg.Body)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,35 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
|
|||
base := "/chronograf/v1/dashboards"
|
||||
cells := make([]dashboardCellResponse, len(dcells))
|
||||
for i, cell := range dcells {
|
||||
if len(cell.Queries) == 0 {
|
||||
cell.Queries = make([]chronograf.DashboardQuery, 0)
|
||||
newCell := chronograf.DashboardCell{}
|
||||
|
||||
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
|
||||
copy(newCell.Queries, cell.Queries)
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
labels := []string{"x", "y", "y2"}
|
||||
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
|
||||
|
||||
newCell.X = cell.X
|
||||
newCell.Y = cell.Y
|
||||
newCell.W = cell.W
|
||||
newCell.H = cell.H
|
||||
newCell.Name = cell.Name
|
||||
newCell.ID = cell.ID
|
||||
newCell.Type = cell.Type
|
||||
|
||||
for _, lbl := range labels {
|
||||
if axis, found := cell.Axes[lbl]; !found {
|
||||
newCell.Axes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
} else {
|
||||
newCell.Axes[lbl] = axis
|
||||
}
|
||||
}
|
||||
|
||||
cells[i] = dashboardCellResponse{
|
||||
DashboardCell: cell,
|
||||
DashboardCell: newCell,
|
||||
Links: dashboardCellLinks{
|
||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
||||
},
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/server"
|
||||
)
|
||||
|
||||
|
@ -20,13 +29,13 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
&chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "100"},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "100"},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "100"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -37,10 +46,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
&chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"axis of evil": chronograf.Axis{
|
||||
Bounds: [2]int64{666, 666},
|
||||
Bounds: []string{"666", "666"},
|
||||
},
|
||||
"axis of awesome": chronograf.Axis{
|
||||
Bounds: [2]int64{1337, 31337},
|
||||
Bounds: []string{"1337", "31337"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -58,3 +67,139 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Service_DashboardCells(t *testing.T) {
|
||||
cellsTests := []struct {
|
||||
name string
|
||||
reqURL *url.URL
|
||||
ctxParams map[string]string
|
||||
mockResponse []chronograf.DashboardCell
|
||||
expected []chronograf.DashboardCell
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
"happy path",
|
||||
&url.URL{
|
||||
Path: "/chronograf/v1/dashboards/1/cells",
|
||||
},
|
||||
map[string]string{
|
||||
"id": "1",
|
||||
},
|
||||
[]chronograf.DashboardCell{},
|
||||
[]chronograf.DashboardCell{},
|
||||
http.StatusOK,
|
||||
},
|
||||
{
|
||||
"cell axes should always be \"x\", \"y\", and \"y2\"",
|
||||
&url.URL{
|
||||
Path: "/chronograf/v1/dashboards/1/cells",
|
||||
},
|
||||
map[string]string{
|
||||
"id": "1",
|
||||
},
|
||||
[]chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "CPU",
|
||||
Type: "bar",
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
Axes: map[string]chronograf.Axis{},
|
||||
},
|
||||
},
|
||||
[]chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "CPU",
|
||||
Type: "bar",
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cellsTests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// setup context with params
|
||||
ctx := context.Background()
|
||||
params := httprouter.Params{}
|
||||
for k, v := range test.ctxParams {
|
||||
params = append(params, httprouter.Param{k, v})
|
||||
}
|
||||
ctx = httprouter.WithParams(ctx, params)
|
||||
|
||||
// setup response recorder and request
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", test.reqURL.RequestURI(), strings.NewReader("")).WithContext(ctx)
|
||||
|
||||
// setup mock DashboardCells store and logger
|
||||
tlog := &mocks.TestLogger{}
|
||||
svc := &server.Service{
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{
|
||||
ID: chronograf.DashboardID(1),
|
||||
Cells: test.mockResponse,
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "empty dashboard",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
Logger: tlog,
|
||||
}
|
||||
|
||||
// invoke DashboardCell handler
|
||||
svc.DashboardCells(rr, req)
|
||||
|
||||
// setup frame to decode response into
|
||||
respFrame := []struct {
|
||||
chronograf.DashboardCell
|
||||
Links json.RawMessage `json:"links"` // ignore links
|
||||
}{}
|
||||
|
||||
// decode response
|
||||
resp := rr.Result()
|
||||
|
||||
if resp.StatusCode != test.expectedCode {
|
||||
tlog.Dump(t)
|
||||
t.Fatalf("%q - Status codes do not match. Want %d (%s), Got %d (%s)", test.name, test.expectedCode, http.StatusText(test.expectedCode), resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respFrame); err != nil {
|
||||
t.Fatalf("%q - Error unmarshaling response body: err: %s", test.name, err)
|
||||
}
|
||||
|
||||
// extract actual
|
||||
actual := []chronograf.DashboardCell{}
|
||||
for _, rsp := range respFrame {
|
||||
actual = append(actual, rsp.DashboardCell)
|
||||
}
|
||||
|
||||
// compare actual and expected
|
||||
if !cmp.Equal(actual, test.expected) {
|
||||
t.Fatalf("%q - Dashboard Cells do not match: diff: %s", test.name, cmp.Diff(actual, test.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,20 +28,19 @@ type getDashboardsResponse struct {
|
|||
|
||||
func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
DashboardDefaults(&d)
|
||||
AddQueryConfigs(&d)
|
||||
cells := newCellResponses(d.ID, d.Cells)
|
||||
templates := newTemplateResponses(d.ID, d.Templates)
|
||||
dd := AddQueryConfigs(DashboardDefaults(d))
|
||||
cells := newCellResponses(dd.ID, dd.Cells)
|
||||
templates := newTemplateResponses(dd.ID, dd.Templates)
|
||||
|
||||
return &dashboardResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
ID: dd.ID,
|
||||
Name: dd.Name,
|
||||
Cells: cells,
|
||||
Templates: templates,
|
||||
Links: dashboardLinks{
|
||||
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
||||
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID),
|
||||
Templates: fmt.Sprintf("%s/%d/templates", base, d.ID),
|
||||
Self: fmt.Sprintf("%s/%d", base, dd.ID),
|
||||
Cells: fmt.Sprintf("%s/%d/cells", base, dd.ID),
|
||||
Templates: fmt.Sprintf("%s/%d/templates", base, dd.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -229,24 +228,36 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
DashboardDefaults(d)
|
||||
(*d) = DashboardDefaults(*d)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DashboardDefaults updates the dashboard with the default values
|
||||
// if none are specified
|
||||
func DashboardDefaults(d *chronograf.Dashboard) {
|
||||
func DashboardDefaults(d chronograf.Dashboard) (newDash chronograf.Dashboard) {
|
||||
newDash.ID = d.ID
|
||||
newDash.Templates = d.Templates
|
||||
newDash.Name = d.Name
|
||||
newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells))
|
||||
|
||||
for i, c := range d.Cells {
|
||||
CorrectWidthHeight(&c)
|
||||
d.Cells[i] = c
|
||||
newDash.Cells[i] = c
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddQueryConfigs updates all the celsl in the dashboard to have query config
|
||||
// objects corresponding to their influxql queries.
|
||||
func AddQueryConfigs(d *chronograf.Dashboard) {
|
||||
func AddQueryConfigs(d chronograf.Dashboard) (newDash chronograf.Dashboard) {
|
||||
newDash.ID = d.ID
|
||||
newDash.Templates = d.Templates
|
||||
newDash.Name = d.Name
|
||||
newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells))
|
||||
|
||||
for i, c := range d.Cells {
|
||||
AddQueryConfig(&c)
|
||||
d.Cells[i] = c
|
||||
newDash.Cells[i] = c
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ func TestDashboardDefaults(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if DashboardDefaults(&tt.d); !reflect.DeepEqual(tt.d, tt.want) {
|
||||
if actual := DashboardDefaults(tt.d); !reflect.DeepEqual(actual, tt.want) {
|
||||
t.Errorf("%q. DashboardDefaults() = %v, want %v", tt.name, tt.d, tt.want)
|
||||
}
|
||||
}
|
||||
|
@ -222,10 +222,11 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "100"},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{2, 95},
|
||||
Bounds: []string{"2", "95"},
|
||||
Label: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -268,10 +269,14 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
Bounds: []string{"0", "100"},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{2, 95},
|
||||
Bounds: []string{"2", "95"},
|
||||
Label: "foo",
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -284,6 +289,17 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
ID: "b",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m",
|
||||
|
|
|
@ -50,7 +50,7 @@ type Server struct {
|
|||
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
|
||||
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
|
||||
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"hunter2\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
|
||||
|
||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
|
|
|
@ -33,4 +33,4 @@ yarn upgrade packageName
|
|||
```
|
||||
|
||||
## Testing
|
||||
Tests can be run via command line with `npm test`, from within the `/ui` directory. For more detailed reporting, use `npm test -- --reporters=verbose`.
|
||||
Tests can be run via command line with `yarn test`, from within the `/ui` directory. For more detailed reporting, use `yarn test -- --reporters=verbose`.
|
||||
|
|
|
@ -16,7 +16,7 @@ module.exports = function(config) {
|
|||
'spec/index.js': ['webpack', 'sourcemap'],
|
||||
},
|
||||
// For more detailed reporting on tests, you can add 'verbose' and/or 'progress'.
|
||||
// This can also be done via the command line with `npm test -- --reporters=verbose`.
|
||||
// This can also be done via the command line with `yarn test -- --reporters=verbose`.
|
||||
reporters: ['dots'],
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.3.5-0",
|
||||
"version": "1.3.6-0",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
@ -9,13 +9,13 @@
|
|||
"url": "github:influxdata/chronograf"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js",
|
||||
"build": "yarn run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js",
|
||||
"build:dev": "node_modules/webpack/bin/webpack.js --config ./webpack/devConfig.js",
|
||||
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
|
||||
"lint": "node_modules/eslint/bin/eslint.js src/",
|
||||
"test": "karma start",
|
||||
"test:lint": "npm run lint; npm run test",
|
||||
"test:dev": "nodemon --exec npm run test:lint",
|
||||
"test:lint": "yarn run lint; yarn run test",
|
||||
"test:dev": "nodemon --exec yarn run test:lint",
|
||||
"clean": "rm -rf build",
|
||||
"storybook": "node ./storybook",
|
||||
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
|
||||
|
|
|
@ -60,6 +60,7 @@ const c1 = {
|
|||
w: 4,
|
||||
h: 4,
|
||||
id: 1,
|
||||
i: 'im-a-cell-id-index',
|
||||
isEditing: false,
|
||||
name: 'Gigawatts',
|
||||
}
|
||||
|
@ -71,18 +72,6 @@ const editingCell = {
|
|||
}
|
||||
|
||||
const cells = [c1]
|
||||
const tempVar = {
|
||||
...d1.templates[0],
|
||||
id: '1',
|
||||
type: 'measurement',
|
||||
label: 'test query',
|
||||
tempVar: '$HOSTS',
|
||||
query: {
|
||||
db: 'db1',
|
||||
text: 'SHOW TAGS WHERE HUNTER = "coo"',
|
||||
},
|
||||
values: ['h1', 'h2', 'h3'],
|
||||
}
|
||||
|
||||
describe('DataExplorer.Reducers.UI', () => {
|
||||
it('can load the dashboards', () => {
|
||||
|
|
|
@ -17,10 +17,18 @@ describe('getRangeForDygraphSpec', () => {
|
|||
|
||||
it('does not get range when a range is provided', () => {
|
||||
const timeSeries = [[date, min], [date, max], [date, mid]]
|
||||
const providedRange = [0, 4]
|
||||
const providedRange = ['0', '4']
|
||||
const actual = getRange(timeSeries, providedRange)
|
||||
|
||||
expect(actual).to.deep.equal(providedRange)
|
||||
expect(actual).to.deep.equal([0, 4])
|
||||
})
|
||||
|
||||
it('does not use the user submitted range if they are equal', () => {
|
||||
const timeSeries = [[date, min], [date, max], [date, mid]]
|
||||
const providedRange = ['0', '0']
|
||||
const actual = getRange(timeSeries, providedRange)
|
||||
|
||||
expect(actual).to.deep.equal([min, max])
|
||||
})
|
||||
|
||||
it('gets the range for multiple timeSeries', () => {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import {Link} from 'react-router'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
|
@ -130,10 +132,7 @@ class AlertsTable extends Component {
|
|||
>
|
||||
{alerts.map(({name, level, time, host, value}) => {
|
||||
return (
|
||||
<div
|
||||
className="alert-history-table--tr"
|
||||
key={`${name}-${level}-${time}-${host}-${value}`}
|
||||
>
|
||||
<div className="alert-history-table--tr" key={uuid.v4()}>
|
||||
<div
|
||||
className="alert-history-table--td"
|
||||
style={{width: colName}}
|
||||
|
|
|
@ -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
|
|
@ -7,12 +7,15 @@ import ResizeContainer from 'shared/components/ResizeContainer'
|
|||
import QueryMaker from 'src/data_explorer/components/QueryMaker'
|
||||
import Visualization from 'src/data_explorer/components/Visualization'
|
||||
import OverlayControls from 'src/dashboards/components/OverlayControls'
|
||||
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
|
||||
|
||||
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import {getQueryConfig} from 'shared/apis'
|
||||
|
||||
import {buildYLabel} from 'shared/presenters'
|
||||
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
|
||||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
|
||||
|
@ -22,17 +25,17 @@ class CellEditorOverlay extends Component {
|
|||
super(props)
|
||||
|
||||
this.queryStateReducer = ::this.queryStateReducer
|
||||
|
||||
this.handleAddQuery = ::this.handleAddQuery
|
||||
this.handleDeleteQuery = ::this.handleDeleteQuery
|
||||
|
||||
this.handleSaveCell = ::this.handleSaveCell
|
||||
|
||||
this.handleSelectGraphType = ::this.handleSelectGraphType
|
||||
this.handleClickDisplayOptionsTab = ::this.handleClickDisplayOptionsTab
|
||||
this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
|
||||
this.handleEditRawText = ::this.handleEditRawText
|
||||
this.handleSetYAxisBounds = ::this.handleSetYAxisBounds
|
||||
this.handleSetLabel = ::this.handleSetLabel
|
||||
|
||||
const {cell: {name, type, queries}} = props
|
||||
const {cell: {name, type, queries, axes}} = props
|
||||
|
||||
const queriesWorkingDraft = _.cloneDeep(
|
||||
queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()}))
|
||||
|
@ -43,9 +46,26 @@ class CellEditorOverlay extends Component {
|
|||
cellWorkingType: type,
|
||||
queriesWorkingDraft,
|
||||
activeQueryIndex: 0,
|
||||
isDisplayOptionsTabActive: false,
|
||||
axes: this.setDefaultLabels(axes, queries),
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultLabels(axes, queries) {
|
||||
if (!queries.length) {
|
||||
return axes
|
||||
}
|
||||
|
||||
if (axes.y.label) {
|
||||
return axes
|
||||
}
|
||||
|
||||
const q = queries[0].queryConfig
|
||||
const label = buildYLabel(q)
|
||||
|
||||
return {...axes, y: {...axes.y, label}}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {status, queryID} = this.props.queryStatus
|
||||
const nextStatus = nextProps.queryStatus
|
||||
|
@ -73,6 +93,24 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleSetYAxisBounds(e) {
|
||||
const {min, max} = e.target.form
|
||||
const {axes} = this.state
|
||||
|
||||
this.setState({
|
||||
axes: {...axes, y: {...axes.y, bounds: [min.value, max.value]}},
|
||||
})
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
handleSetLabel(e) {
|
||||
const {label} = e.target.form
|
||||
const {axes} = this.state
|
||||
|
||||
this.setState({axes: {...axes, y: {...axes.y, label: label.value}}})
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
handleAddQuery(options) {
|
||||
const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
|
||||
const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
|
||||
|
@ -87,31 +125,44 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleSaveCell() {
|
||||
const {queriesWorkingDraft, cellWorkingType, cellWorkingName} = this.state
|
||||
const {
|
||||
queriesWorkingDraft,
|
||||
cellWorkingType: type,
|
||||
cellWorkingName: name,
|
||||
axes,
|
||||
} = this.state
|
||||
|
||||
const {cell} = this.props
|
||||
|
||||
const newCell = _.cloneDeep(cell)
|
||||
newCell.name = cellWorkingName
|
||||
newCell.type = cellWorkingType
|
||||
newCell.queries = queriesWorkingDraft.map(q => {
|
||||
const queries = queriesWorkingDraft.map(q => {
|
||||
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
|
||||
const query = q.rawText || buildInfluxQLQuery(timeRange, q)
|
||||
const label = q.rawText ? '' : `${q.measurement}.${q.fields[0].field}`
|
||||
|
||||
return {
|
||||
queryConfig: q,
|
||||
query,
|
||||
label,
|
||||
}
|
||||
})
|
||||
|
||||
this.props.onSave(newCell)
|
||||
this.props.onSave({
|
||||
...cell,
|
||||
name,
|
||||
type,
|
||||
queries,
|
||||
axes,
|
||||
})
|
||||
}
|
||||
|
||||
handleSelectGraphType(graphType) {
|
||||
this.setState({cellWorkingType: graphType})
|
||||
}
|
||||
|
||||
handleClickDisplayOptionsTab(isDisplayOptionsTabActive) {
|
||||
return () => {
|
||||
this.setState({isDisplayOptionsTabActive})
|
||||
}
|
||||
}
|
||||
|
||||
handleSetActiveQueryIndex(activeQueryIndex) {
|
||||
this.setState({activeQueryIndex})
|
||||
}
|
||||
|
@ -146,7 +197,9 @@ class CellEditorOverlay extends Component {
|
|||
activeQueryIndex,
|
||||
cellWorkingName,
|
||||
cellWorkingType,
|
||||
isDisplayOptionsTabActive,
|
||||
queriesWorkingDraft,
|
||||
axes,
|
||||
} = this.state
|
||||
|
||||
const queryActions = {
|
||||
|
@ -177,27 +230,36 @@ class CellEditorOverlay extends Component {
|
|||
cellType={cellWorkingType}
|
||||
cellName={cellWorkingName}
|
||||
editQueryStatus={editQueryStatus}
|
||||
axes={axes}
|
||||
views={[]}
|
||||
/>
|
||||
<div className="overlay-technology--editor">
|
||||
<OverlayControls
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
isDisplayOptionsTabActive={isDisplayOptionsTabActive}
|
||||
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
|
||||
onCancel={onCancel}
|
||||
onSave={this.handleSaveCell}
|
||||
isSavable={queriesWorkingDraft.every(isQuerySavable)}
|
||||
/>
|
||||
<QueryMaker
|
||||
source={source}
|
||||
templates={templates}
|
||||
queries={queriesWorkingDraft}
|
||||
actions={queryActions}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
setActiveQueryIndex={this.handleSetActiveQueryIndex}
|
||||
onDeleteQuery={this.handleDeleteQuery}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
/>
|
||||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onSetRange={this.handleSetYAxisBounds}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
axes={axes}
|
||||
/>
|
||||
: <QueryMaker
|
||||
source={source}
|
||||
templates={templates}
|
||||
queries={queriesWorkingDraft}
|
||||
actions={queryActions}
|
||||
autoRefresh={autoRefresh}
|
||||
timeRange={timeRange}
|
||||
setActiveQueryIndex={this.handleSetActiveQueryIndex}
|
||||
onDeleteQuery={this.handleDeleteQuery}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
/>}
|
||||
</div>
|
||||
</ResizeContainer>
|
||||
</div>
|
||||
|
@ -232,6 +294,7 @@ CellEditorOverlay.propTypes = {
|
|||
queryID: string,
|
||||
status: shape({}),
|
||||
}).isRequired,
|
||||
dashboardID: string.isRequired,
|
||||
}
|
||||
|
||||
export default CellEditorOverlay
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -3,51 +3,51 @@ import classnames from 'classnames'
|
|||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
|
||||
import graphTypes from 'hson!shared/data/graphTypes.hson'
|
||||
|
||||
const OverlayControls = props => {
|
||||
const {
|
||||
onCancel,
|
||||
onSave,
|
||||
selectedGraphType,
|
||||
onSelectGraphType,
|
||||
isSavable,
|
||||
} = props
|
||||
return (
|
||||
<div className="overlay-controls">
|
||||
<h3 className="overlay--graph-name">Cell Editor</h3>
|
||||
<div className="overlay-controls--right">
|
||||
<p>Visualization Type:</p>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
{graphTypes.map(graphType =>
|
||||
<li
|
||||
key={graphType.type}
|
||||
className={classnames({
|
||||
active: graphType.type === selectedGraphType,
|
||||
})}
|
||||
onClick={() => onSelectGraphType(graphType.type)}
|
||||
>
|
||||
{graphType.menuOption}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ConfirmButtons
|
||||
onCancel={onCancel}
|
||||
onConfirm={onSave}
|
||||
isDisabled={!isSavable}
|
||||
/>
|
||||
</div>
|
||||
const OverlayControls = ({
|
||||
onCancel,
|
||||
onSave,
|
||||
isDisplayOptionsTabActive,
|
||||
onClickDisplayOptions,
|
||||
isSavable,
|
||||
}) =>
|
||||
<div className="overlay-controls">
|
||||
<h3 className="overlay--graph-name">Cell Editor</h3>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
key="queries"
|
||||
className={classnames({
|
||||
active: !isDisplayOptionsTabActive,
|
||||
})}
|
||||
onClick={onClickDisplayOptions(false)}
|
||||
>
|
||||
Queries
|
||||
</li>
|
||||
<li
|
||||
key="displayOptions"
|
||||
className={classnames({
|
||||
active: isDisplayOptionsTabActive,
|
||||
})}
|
||||
onClick={onClickDisplayOptions(true)}
|
||||
>
|
||||
Display Options
|
||||
</li>
|
||||
</ul>
|
||||
<div className="overlay-controls--right">
|
||||
<ConfirmButtons
|
||||
onCancel={onCancel}
|
||||
onConfirm={onSave}
|
||||
isDisabled={!isSavable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
const {func, bool} = PropTypes
|
||||
|
||||
OverlayControls.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
selectedGraphType: string.isRequired,
|
||||
onSelectGraphType: func.isRequired,
|
||||
isDisplayOptionsTabActive: bool.isRequired,
|
||||
onClickDisplayOptions: func.isRequired,
|
||||
isSavable: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -209,7 +209,11 @@ class DashboardPage extends Component {
|
|||
const dygraphs = [...this.state.dygraphs, dygraph]
|
||||
const {dashboards, params} = this.props
|
||||
const dashboard = dashboards.find(d => d.id === +params.dashboardID)
|
||||
if (dashboard && dygraphs.length === dashboard.cells.length) {
|
||||
if (
|
||||
dashboard &&
|
||||
dygraphs.length === dashboard.cells.length &&
|
||||
dashboard.cells.length > 1
|
||||
) {
|
||||
Dygraph.synchronize(dygraphs, {
|
||||
selection: true,
|
||||
zoom: false,
|
||||
|
@ -248,7 +252,7 @@ class DashboardPage extends Component {
|
|||
inPresentationMode,
|
||||
handleChooseAutoRefresh,
|
||||
handleClickPresentationButton,
|
||||
params: {sourceID},
|
||||
params: {sourceID, dashboardID},
|
||||
} = this.props
|
||||
|
||||
const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant'
|
||||
|
@ -322,6 +326,7 @@ class DashboardPage extends Component {
|
|||
{selectedCell
|
||||
? <CellEditorOverlay
|
||||
source={source}
|
||||
dashboardID={dashboardID}
|
||||
templates={templatesIncludingDashTime}
|
||||
cell={selectedCell}
|
||||
timeRange={timeRange}
|
||||
|
@ -372,8 +377,8 @@ class DashboardPage extends Component {
|
|||
autoRefresh={autoRefresh}
|
||||
synchronizer={this.synchronizer}
|
||||
onAddCell={this.handleAddCell}
|
||||
inPresentationMode={inPresentationMode}
|
||||
onEditCell={this.handleEditDashboardCell}
|
||||
inPresentationMode={inPresentationMode}
|
||||
onPositionChange={this.handleUpdatePosition}
|
||||
onDeleteCell={this.handleDeleteDashboardCell}
|
||||
onUpdateCell={this.handleUpdateDashboardCell}
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
]
|
|
@ -12,7 +12,6 @@ import {Table, Column, Cell} from 'fixed-data-table'
|
|||
|
||||
const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes
|
||||
|
||||
const defaultTableHeight = 1000
|
||||
const emptySeries = {columns: [], values: []}
|
||||
|
||||
const CustomCell = React.createClass({
|
||||
|
@ -64,7 +63,7 @@ const ChronoTable = React.createClass({
|
|||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
height: defaultTableHeight,
|
||||
height: 500,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -139,11 +138,11 @@ const ChronoTable = React.createClass({
|
|||
const maximumTabsCount = 11
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
const stylePixelOffset = 136
|
||||
const rowHeight = 34
|
||||
const defaultColumnWidth = 200
|
||||
const headerHeight = 30
|
||||
const minWidth = 70
|
||||
const rowHeight = 34
|
||||
const headerHeight = 30
|
||||
const stylePixelOffset = 125
|
||||
const defaultColumnWidth = 200
|
||||
const styleAdjustedHeight = height - stylePixelOffset
|
||||
const width =
|
||||
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import Table from './Table'
|
||||
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||
import LineGraph from 'shared/components/LineGraph'
|
||||
import SingleStat from 'shared/components/SingleStat'
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
|
||||
const VisView = ({
|
||||
axes,
|
||||
view,
|
||||
queries,
|
||||
cellType,
|
||||
|
@ -16,7 +13,7 @@ const VisView = ({
|
|||
heightPixels,
|
||||
editQueryStatus,
|
||||
activeQueryIndex,
|
||||
isInDataExplorer,
|
||||
resizerBottomHeight,
|
||||
}) => {
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
|
@ -34,46 +31,30 @@ const VisView = ({
|
|||
return (
|
||||
<Table
|
||||
query={query}
|
||||
height={heightPixels}
|
||||
height={resizerBottomHeight}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (cellType === 'single-stat') {
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
queries={queries.length ? [queries[0]] : []}
|
||||
autoRefresh={autoRefresh}
|
||||
templates={templates}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const displayOptions = {
|
||||
stepPlot: cellType === 'line-stepplot',
|
||||
stackedGraph: cellType === 'line-stacked',
|
||||
}
|
||||
|
||||
return (
|
||||
<RefreshingLineGraph
|
||||
<RefreshingGraph
|
||||
axes={axes}
|
||||
type={cellType}
|
||||
queries={queries}
|
||||
autoRefresh={autoRefresh}
|
||||
templates={templates}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
showSingleStat={cellType === 'line-plus-single-stat'}
|
||||
isBarGraph={cellType === 'bar'}
|
||||
displayOptions={displayOptions}
|
||||
cellHeight={heightPixels}
|
||||
autoRefresh={autoRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
VisView.propTypes = {
|
||||
view: string.isRequired,
|
||||
axes: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
cellType: string,
|
||||
templates: arrayOf(shape()),
|
||||
|
@ -81,7 +62,7 @@ VisView.propTypes = {
|
|||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
activeQueryIndex: number,
|
||||
isInDataExplorer: bool,
|
||||
resizerBottomHeight: number,
|
||||
}
|
||||
|
||||
export default VisView
|
||||
|
|
|
@ -26,6 +26,12 @@ const Visualization = React.createClass({
|
|||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
views: arrayOf(string).isRequired,
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: arrayOf(string),
|
||||
}),
|
||||
}),
|
||||
resizerBottomHeight: number,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
|
@ -48,6 +54,7 @@ const Visualization = React.createClass({
|
|||
getDefaultProps() {
|
||||
return {
|
||||
cellName: '',
|
||||
cellType: '',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -76,6 +83,7 @@ const Visualization = React.createClass({
|
|||
|
||||
render() {
|
||||
const {
|
||||
axes,
|
||||
views,
|
||||
height,
|
||||
cellType,
|
||||
|
@ -88,16 +96,19 @@ const Visualization = React.createClass({
|
|||
editQueryStatus,
|
||||
activeQueryIndex,
|
||||
isInDataExplorer,
|
||||
resizerBottomHeight,
|
||||
} = this.props
|
||||
const {source: {links: {proxy}}} = this.context
|
||||
const {view} = this.state
|
||||
|
||||
const statements = queryConfigs.map(query => {
|
||||
const text = query.rawText || buildInfluxQLQuery(timeRange, query)
|
||||
return {text, id: query.id}
|
||||
const text =
|
||||
query.rawText || buildInfluxQLQuery(query.range || timeRange, query)
|
||||
return {text, id: query.id, queryConfig: query}
|
||||
})
|
||||
|
||||
const queries = statements.filter(s => s.text !== null).map(s => {
|
||||
return {host: [proxy], text: s.text, id: s.id}
|
||||
return {host: [proxy], text: s.text, id: s.id, queryConfig: s.queryConfig}
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -116,6 +127,7 @@ const Visualization = React.createClass({
|
|||
>
|
||||
<VisView
|
||||
view={view}
|
||||
axes={axes}
|
||||
queries={queries}
|
||||
templates={templates}
|
||||
cellType={cellType}
|
||||
|
@ -124,6 +136,7 @@ const Visualization = React.createClass({
|
|||
editQueryStatus={editQueryStatus}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
resizerBottomHeight={resizerBottomHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ const WriteDataBody = ({
|
|||
ref={fileInput}
|
||||
accept="text/*, application/gzip"
|
||||
/>
|
||||
<button className="btn btn-sm btn-primary" onClick={handleFileOpen}>
|
||||
<button className="btn btn-md btn-primary" onClick={handleFileOpen}>
|
||||
{uploadContent ? 'Upload a Different File' : 'Upload a File'}
|
||||
</button>
|
||||
{uploadContent
|
||||
|
|
|
@ -4,6 +4,7 @@ import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
|||
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
|
||||
|
||||
const {
|
||||
array,
|
||||
arrayOf,
|
||||
bool,
|
||||
element,
|
||||
|
@ -43,6 +44,12 @@ const AutoRefresh = ComposedComponent => {
|
|||
text: string,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
axes: shape({
|
||||
bounds: shape({
|
||||
y: array,
|
||||
y2: array,
|
||||
}),
|
||||
}),
|
||||
editQueryStatus: func,
|
||||
},
|
||||
|
||||
|
|
|
@ -9,6 +9,13 @@ import getRange from 'shared/parsing/getRangeForDygraph'
|
|||
|
||||
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
|
||||
import DygraphLegend from 'src/shared/components/DygraphLegend'
|
||||
import {buildYLabel} from 'shared/presenters'
|
||||
|
||||
const hasherino = (str, len) =>
|
||||
str
|
||||
.split('')
|
||||
.map(char => char.charCodeAt(0))
|
||||
.reduce((hash, code) => hash + code, 0) % len
|
||||
|
||||
export default class Dygraph extends Component {
|
||||
constructor(props) {
|
||||
|
@ -35,12 +42,14 @@ export default class Dygraph extends Component {
|
|||
this.handleHideLegend = ::this.handleHideLegend
|
||||
this.handleToggleFilter = ::this.handleToggleFilter
|
||||
this.visibility = ::this.visibility
|
||||
this.getLabel = ::this.getLabel
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
containerStyle: {},
|
||||
isGraphFilled: true,
|
||||
overrideLineColors: null,
|
||||
dygraphRef: () => {},
|
||||
}
|
||||
|
||||
getTimeSeries() {
|
||||
|
@ -50,11 +59,23 @@ export default class Dygraph extends Component {
|
|||
return timeSeries.length ? timeSeries : [[0]]
|
||||
}
|
||||
|
||||
getLabel(axis) {
|
||||
const {axes, queries} = this.props
|
||||
const label = _.get(axes, [axis, 'label'], '')
|
||||
const queryConfig = _.get(queries, ['0', 'queryConfig'], false)
|
||||
|
||||
if (label || !queryConfig) {
|
||||
return label
|
||||
}
|
||||
|
||||
return buildYLabel(queryConfig)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const timeSeries = this.getTimeSeries()
|
||||
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
|
||||
const {
|
||||
ranges,
|
||||
axes,
|
||||
dygraphSeries,
|
||||
ruleValues,
|
||||
overrideLineColors,
|
||||
|
@ -65,18 +86,29 @@ export default class Dygraph extends Component {
|
|||
|
||||
const graphRef = this.graphRef
|
||||
const legendRef = this.legendRef
|
||||
let finalLineColors = overrideLineColors
|
||||
const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
|
||||
|
||||
if (finalLineColors === null) {
|
||||
finalLineColors = LINE_COLORS
|
||||
const hashColorDygraphSeries = {}
|
||||
const {length} = finalLineColors
|
||||
|
||||
for (const seriesName in dygraphSeries) {
|
||||
const series = dygraphSeries[seriesName]
|
||||
const hashIndex = hasherino(seriesName, length)
|
||||
const color = finalLineColors[hashIndex]
|
||||
hashColorDygraphSeries[seriesName] = {...series, color}
|
||||
}
|
||||
|
||||
const yAxis = _.get(axes, ['y', 'bounds'], [null, null])
|
||||
const y2Axis = _.get(axes, ['y2', 'bounds'], undefined)
|
||||
|
||||
const defaultOptions = {
|
||||
plugins: [
|
||||
new Dygraphs.Plugins.Crosshair({
|
||||
direction: 'vertical',
|
||||
}),
|
||||
],
|
||||
plugins: isBarGraph
|
||||
? []
|
||||
: [
|
||||
new Dygraphs.Plugins.Crosshair({
|
||||
direction: 'vertical',
|
||||
}),
|
||||
],
|
||||
labelsSeparateLines: false,
|
||||
labelsKMB: true,
|
||||
rightGap: 0,
|
||||
|
@ -85,22 +117,22 @@ export default class Dygraph extends Component {
|
|||
fillGraph: isGraphFilled,
|
||||
axisLineWidth: 2,
|
||||
gridLineWidth: 1,
|
||||
highlightCircleSize: 3,
|
||||
highlightCircleSize: isBarGraph ? 0 : 3,
|
||||
animatedZooms: true,
|
||||
hideOverlayOnMouseOut: false,
|
||||
colors: finalLineColors,
|
||||
series: dygraphSeries,
|
||||
series: hashColorDygraphSeries,
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, ranges.y, ruleValues),
|
||||
valueRange: getRange(timeSeries, yAxis, ruleValues),
|
||||
},
|
||||
y2: {
|
||||
valueRange: getRange(timeSeries, ranges.y2),
|
||||
valueRange: getRange(timeSeries, y2Axis),
|
||||
},
|
||||
},
|
||||
highlightSeriesOpts: {
|
||||
strokeWidth: 2,
|
||||
highlightCircleSize: 5,
|
||||
highlightCircleSize: isBarGraph ? 0 : 5,
|
||||
},
|
||||
legendFormatter: legend => {
|
||||
if (!legend.x) {
|
||||
|
@ -235,11 +267,12 @@ export default class Dygraph extends Component {
|
|||
componentDidUpdate() {
|
||||
const {
|
||||
labels,
|
||||
ranges,
|
||||
axes,
|
||||
options,
|
||||
dygraphSeries,
|
||||
ruleValues,
|
||||
isBarGraph,
|
||||
overrideLineColors,
|
||||
} = this.props
|
||||
|
||||
const dygraph = this.dygraph
|
||||
|
@ -249,22 +282,39 @@ export default class Dygraph extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
const y = _.get(axes, ['y', 'bounds'], [null, null])
|
||||
const y2 = _.get(axes, ['y2', 'bounds'], undefined)
|
||||
const timeSeries = this.getTimeSeries()
|
||||
const ylabel = this.getLabel('y')
|
||||
const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
|
||||
|
||||
const hashColorDygraphSeries = {}
|
||||
const {length} = finalLineColors
|
||||
|
||||
for (const seriesName in dygraphSeries) {
|
||||
const series = dygraphSeries[seriesName]
|
||||
const hashIndex = hasherino(seriesName, length)
|
||||
const color = finalLineColors[hashIndex]
|
||||
hashColorDygraphSeries[seriesName] = {...series, color}
|
||||
}
|
||||
|
||||
const updateOptions = {
|
||||
labels,
|
||||
file: timeSeries,
|
||||
ylabel,
|
||||
axes: {
|
||||
y: {
|
||||
valueRange: getRange(timeSeries, ranges.y, ruleValues),
|
||||
valueRange: getRange(timeSeries, y, ruleValues),
|
||||
},
|
||||
y2: {
|
||||
valueRange: getRange(timeSeries, ranges.y2),
|
||||
valueRange: getRange(timeSeries, y2),
|
||||
},
|
||||
},
|
||||
stepPlot: options.stepPlot,
|
||||
stackedGraph: options.stackedGraph,
|
||||
underlayCallback: options.underlayCallback,
|
||||
series: dygraphSeries,
|
||||
colors: finalLineColors,
|
||||
series: hashColorDygraphSeries,
|
||||
plotter: isBarGraph ? multiColumnBarPlotter : null,
|
||||
visibility: this.visibility(),
|
||||
}
|
||||
|
@ -350,6 +400,7 @@ export default class Dygraph extends Component {
|
|||
<div
|
||||
ref={r => {
|
||||
this.graphRef = r
|
||||
this.props.dygraphRef(r)
|
||||
}}
|
||||
style={this.props.containerStyle}
|
||||
className="dygraph-child-container"
|
||||
|
@ -359,13 +410,18 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {array, arrayOf, func, number, bool, shape, string} = PropTypes
|
||||
const {array, arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
Dygraph.propTypes = {
|
||||
ranges: shape({
|
||||
y: arrayOf(number),
|
||||
y2: arrayOf(number),
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: array,
|
||||
}),
|
||||
y2: shape({
|
||||
bounds: array,
|
||||
}),
|
||||
}),
|
||||
queries: arrayOf(shape),
|
||||
timeSeries: array.isRequired,
|
||||
labels: array.isRequired,
|
||||
options: shape({}),
|
||||
|
@ -384,4 +440,5 @@ Dygraph.propTypes = {
|
|||
}),
|
||||
synchronizer: func,
|
||||
setResolution: func,
|
||||
dygraphRef: func,
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ class LayoutRenderer extends Component {
|
|||
} = this.props
|
||||
|
||||
return cells.map(cell => {
|
||||
const {type, h} = cell
|
||||
const {type, h, axes} = cell
|
||||
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
|
@ -175,6 +175,7 @@ class LayoutRenderer extends Component {
|
|||
type={type}
|
||||
queries={this.standardizeQueries(cell, source)}
|
||||
cellHeight={h}
|
||||
axes={axes}
|
||||
/>}
|
||||
</NameableGraph>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, {PropTypes} from 'react'
|
|||
import Dygraph from 'shared/components/Dygraph'
|
||||
import classnames from 'classnames'
|
||||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
import _ from 'lodash'
|
||||
|
||||
import timeSeriesToDygraph from 'utils/timeSeriesToDygraph'
|
||||
import lastValues from 'shared/parsing/lastValues'
|
||||
|
@ -15,9 +14,15 @@ export default React.createClass({
|
|||
displayName: 'LineGraph',
|
||||
propTypes: {
|
||||
data: arrayOf(shape({}).isRequired).isRequired,
|
||||
ranges: shape({
|
||||
y: arrayOf(number),
|
||||
y2: arrayOf(number),
|
||||
axes: shape({
|
||||
y: shape({
|
||||
bounds: array,
|
||||
label: string,
|
||||
}),
|
||||
y2: shape({
|
||||
bounds: array,
|
||||
label: string,
|
||||
}),
|
||||
}),
|
||||
title: string,
|
||||
isFetchingInitially: bool,
|
||||
|
@ -81,15 +86,15 @@ export default React.createClass({
|
|||
render() {
|
||||
const {
|
||||
data,
|
||||
ranges,
|
||||
axes,
|
||||
isFetchingInitially,
|
||||
isRefreshing,
|
||||
isGraphFilled,
|
||||
isBarGraph,
|
||||
overrideLineColors,
|
||||
title,
|
||||
underlayCallback,
|
||||
queries,
|
||||
underlayCallback,
|
||||
showSingleStat,
|
||||
displayOptions,
|
||||
ruleValues,
|
||||
|
@ -120,8 +125,6 @@ export default React.createClass({
|
|||
axisLabelWidth: 60,
|
||||
drawAxesAtZero: true,
|
||||
underlayCallback,
|
||||
ylabel: _.get(queries, ['0', 'label'], ''),
|
||||
y2label: _.get(queries, ['1', 'label'], ''),
|
||||
...displayOptions,
|
||||
}
|
||||
|
||||
|
@ -151,26 +154,25 @@ export default React.createClass({
|
|||
roundedValue = Math.round(+lastValue * precision) / precision
|
||||
}
|
||||
|
||||
const lineColors = showSingleStat
|
||||
? singleStatLineColors
|
||||
: overrideLineColors
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('dygraph', {
|
||||
'graph--hasYLabel': !!(options.ylabel || options.y2label),
|
||||
})}
|
||||
style={{height: '100%'}}
|
||||
>
|
||||
<div className={`dygraph ${this.yLabelClass()}`} style={{height: '100%'}}>
|
||||
{isRefreshing ? this.renderSpinner() : null}
|
||||
<Dygraph
|
||||
axes={axes}
|
||||
queries={queries}
|
||||
dygraphRef={this.dygraphRefFunc}
|
||||
containerStyle={{width: '100%', height: '100%'}}
|
||||
overrideLineColors={
|
||||
showSingleStat ? singleStatLineColors : overrideLineColors
|
||||
}
|
||||
overrideLineColors={lineColors}
|
||||
isGraphFilled={showSingleStat ? false : isGraphFilled}
|
||||
isBarGraph={isBarGraph}
|
||||
timeSeries={timeSeries}
|
||||
labels={labels}
|
||||
options={showSingleStat ? singleStatOptions : options}
|
||||
dygraphSeries={dygraphSeries}
|
||||
ranges={ranges || this.getRanges()}
|
||||
ruleValues={ruleValues}
|
||||
synchronizer={synchronizer}
|
||||
timeRange={timeRange}
|
||||
|
@ -193,6 +195,26 @@ export default React.createClass({
|
|||
)
|
||||
},
|
||||
|
||||
yLabelClass() {
|
||||
const dygraph = this.dygraphRef
|
||||
|
||||
if (!dygraph) {
|
||||
return 'graph--hasYLabel'
|
||||
}
|
||||
|
||||
const label = dygraph.querySelector('.dygraph-ylabel')
|
||||
|
||||
if (!label) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return 'graph--hasYLabel'
|
||||
},
|
||||
|
||||
dygraphRefFunc(dygraphRef) {
|
||||
this.dygraphRef = dygraphRef
|
||||
},
|
||||
|
||||
renderSpinner() {
|
||||
return (
|
||||
<div className="graph-panel__refreshing">
|
||||
|
@ -202,25 +224,4 @@ export default React.createClass({
|
|||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
getRanges() {
|
||||
const {queries} = this.props
|
||||
if (!queries) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const ranges = {}
|
||||
const q0 = queries[0]
|
||||
const q1 = queries[1]
|
||||
|
||||
if (q0 && q0.range) {
|
||||
ranges.y = [q0.range.lower, q0.range.upper]
|
||||
}
|
||||
|
||||
if (q1 && q1.range) {
|
||||
ranges.y2 = [q1.range.lower, q1.range.upper]
|
||||
}
|
||||
|
||||
return ranges
|
||||
},
|
||||
})
|
||||
|
|
|
@ -8,13 +8,15 @@ const RefreshingLineGraph = AutoRefresh(LineGraph)
|
|||
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
||||
|
||||
const RefreshingGraph = ({
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
templates,
|
||||
synchronizer,
|
||||
axes,
|
||||
type,
|
||||
queries,
|
||||
templates,
|
||||
timeRange,
|
||||
cellHeight,
|
||||
autoRefresh,
|
||||
synchronizer,
|
||||
editQueryStatus,
|
||||
}) => {
|
||||
if (type === 'single-stat') {
|
||||
return (
|
||||
|
@ -42,6 +44,8 @@ const RefreshingGraph = ({
|
|||
isBarGraph={type === 'bar'}
|
||||
displayOptions={displayOptions}
|
||||
synchronizer={synchronizer}
|
||||
editQueryStatus={editQueryStatus}
|
||||
axes={axes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -56,8 +60,10 @@ RefreshingGraph.propTypes = {
|
|||
templates: arrayOf(shape()),
|
||||
synchronizer: func,
|
||||
type: string.isRequired,
|
||||
cellHeight: number,
|
||||
axes: shape(),
|
||||
queries: arrayOf(shape()).isRequired,
|
||||
cellHeight: number.isRequired,
|
||||
editQueryStatus: func,
|
||||
}
|
||||
|
||||
export default RefreshingGraph
|
||||
|
|
|
@ -31,6 +31,12 @@ class ResizeContainer extends Component {
|
|||
initialBottomHeight: defaultInitialBottomHeight,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
bottomHeightPixels: this.bottom.getBoundingClientRect().height,
|
||||
})
|
||||
}
|
||||
|
||||
handleStartDrag() {
|
||||
this.setState({isDragging: true})
|
||||
}
|
||||
|
@ -51,7 +57,7 @@ class ResizeContainer extends Component {
|
|||
const {minTopHeight, minBottomHeight} = this.props
|
||||
const oneHundred = 100
|
||||
const containerHeight = parseInt(
|
||||
getComputedStyle(this.refs.resizeContainer).height,
|
||||
getComputedStyle(this.resizeContainer).height,
|
||||
10
|
||||
)
|
||||
// verticalOffset moves the resize handle as many pixels as the page-heading is taking up.
|
||||
|
@ -85,11 +91,12 @@ class ResizeContainer extends Component {
|
|||
this.setState({
|
||||
topHeight: `${newTopPanelPercent}%`,
|
||||
bottomHeight: `${newBottomPanelPercent}%`,
|
||||
bottomHeightPixels,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {topHeight, bottomHeight, isDragging} = this.state
|
||||
const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state
|
||||
const {containerClass, children} = this.props
|
||||
|
||||
if (React.Children.count(children) > maximumNumChildren) {
|
||||
|
@ -107,10 +114,12 @@ class ResizeContainer extends Component {
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
onMouseUp={this.handleStopDrag}
|
||||
onMouseMove={this.handleDrag}
|
||||
ref="resizeContainer"
|
||||
ref={r => (this.resizeContainer = r)}
|
||||
>
|
||||
<div className="resize--top" style={{height: topHeight}}>
|
||||
{React.cloneElement(children[0])}
|
||||
{React.cloneElement(children[0], {
|
||||
resizerBottomHeight: bottomHeightPixels,
|
||||
})}
|
||||
</div>
|
||||
<ResizeHandle
|
||||
isDragging={isDragging}
|
||||
|
@ -120,8 +129,11 @@ class ResizeContainer extends Component {
|
|||
<div
|
||||
className="resize--bottom"
|
||||
style={{height: bottomHeight, top: topHeight}}
|
||||
ref={r => (this.bottom = r)}
|
||||
>
|
||||
{React.cloneElement(children[1])}
|
||||
{React.cloneElement(children[1], {
|
||||
resizerBottomHeight: bottomHeightPixels,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[
|
||||
{type: "line", menuOption: "Line"},
|
||||
{type: "line-stacked", menuOption: "Stacked"},
|
||||
{type: "line-stepplot", menuOption: "Step-Plot"},
|
||||
{type: "single-stat", menuOption: "SingleStat"},
|
||||
{type: "line-plus-single-stat", menuOption: "Line + Stat"},
|
||||
{type: "bar", menuOption: "Bar"},
|
||||
{type: "line", menuOption: "Line", graphic: "Wogglez"},
|
||||
{type: "line-stacked", menuOption: "Stacked", graphic: "Rogglez"},
|
||||
{type: "line-stepplot", menuOption: "Step-Plot", graphic: "Fogglez"},
|
||||
{type: "single-stat", menuOption: "SingleStat", graphic: "Bogglez"},
|
||||
{type: "line-plus-single-stat", menuOption: "Line + Stat", graphic: "Togglez"},
|
||||
{type: "bar", menuOption: "Bar", graphic: "Zogglez"},
|
||||
]
|
||||
|
|
|
@ -26,7 +26,7 @@ export const darkenColor = colorStr => {
|
|||
return `rgb(${color.r},${color.g},${color.b})`
|
||||
}
|
||||
|
||||
// Bar Graph code below is from http://dygraphs.com/tests/plotters.html
|
||||
// Bar Graph code below is adapted from http://dygraphs.com/tests/plotters.html
|
||||
export const multiColumnBarPlotter = e => {
|
||||
// We need to handle all the series simultaneously.
|
||||
if (e.seriesIndex !== 0) {
|
||||
|
@ -51,24 +51,34 @@ export const multiColumnBarPlotter = e => {
|
|||
}
|
||||
}
|
||||
|
||||
const barWidth = Math.floor(2.0 / 3 * minSep)
|
||||
// calculate bar width using some graphics math while
|
||||
// ensuring a bar is never smaller than one px, so it is always rendered
|
||||
const barWidth = Math.max(Math.floor(2.0 / 3.0 * minSep), 1.0)
|
||||
|
||||
const fillColors = []
|
||||
const strokeColors = g.getColors()
|
||||
|
||||
let selPointX
|
||||
if (g.selPoints_ && g.selPoints_.length) {
|
||||
selPointX = g.selPoints_[0].canvasx
|
||||
}
|
||||
|
||||
for (let i = 0; i < strokeColors.length; i++) {
|
||||
fillColors.push(darkenColor(strokeColors[i]))
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2
|
||||
|
||||
for (let j = 0; j < sets.length; j++) {
|
||||
ctx.fillStyle = fillColors[j]
|
||||
ctx.strokeStyle = strokeColors[j]
|
||||
for (let i = 0; i < sets[j].length; i++) {
|
||||
const p = sets[j][i]
|
||||
const centerX = p.canvasx
|
||||
ctx.fillStyle = fillColors[j]
|
||||
const xLeft =
|
||||
sets.length === 1
|
||||
? centerX - barWidth / 2
|
||||
: centerX - barWidth / 2 * (1 - j / (sets.length - 1))
|
||||
? centerX - barWidth
|
||||
: centerX - barWidth * (1 - j / sets.length)
|
||||
|
||||
ctx.fillRect(
|
||||
xLeft,
|
||||
|
@ -77,12 +87,15 @@ export const multiColumnBarPlotter = e => {
|
|||
yBottom - p.canvasy
|
||||
)
|
||||
|
||||
ctx.strokeRect(
|
||||
xLeft,
|
||||
p.canvasy,
|
||||
barWidth / sets.length,
|
||||
yBottom - p.canvasy
|
||||
)
|
||||
// hover highlighting
|
||||
if (selPointX === centerX) {
|
||||
ctx.strokeRect(
|
||||
xLeft,
|
||||
p.canvasy,
|
||||
barWidth / sets.length,
|
||||
yBottom - p.canvasy
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
const PADDING_FACTOR = 0.1
|
||||
|
||||
export default function getRange(
|
||||
timeSeries,
|
||||
override,
|
||||
ruleValues = {value: null, rangeValue: null}
|
||||
) {
|
||||
if (override) {
|
||||
return override
|
||||
const considerEmpty = (userNumber, number) => {
|
||||
if (userNumber === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (userNumber) {
|
||||
return +userNumber
|
||||
}
|
||||
|
||||
return number
|
||||
}
|
||||
|
||||
const getRange = (
|
||||
timeSeries,
|
||||
userSelectedRange = [null, null],
|
||||
ruleValues = {value: null, rangeValue: null}
|
||||
) => {
|
||||
const {value, rangeValue, operator} = ruleValues
|
||||
const [userMin, userMax] = userSelectedRange
|
||||
|
||||
const subtractPadding = val => +val - Math.abs(val * PADDING_FACTOR)
|
||||
const addPadding = val => +val + Math.abs(val * PADDING_FACTOR)
|
||||
|
@ -52,10 +61,22 @@ export default function getRange(
|
|||
[null, null]
|
||||
)
|
||||
|
||||
const [min, max] = range
|
||||
|
||||
// If time series is such that min and max are equal use Dygraph defaults
|
||||
if (range[0] === range[1]) {
|
||||
if (min === max) {
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
return range
|
||||
if (userMin === userMax) {
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
if (userMin && userMax) {
|
||||
return [considerEmpty(userMin), considerEmpty(userMax)]
|
||||
}
|
||||
|
||||
return [considerEmpty(userMin, min), considerEmpty(userMax, max)]
|
||||
}
|
||||
|
||||
export default getRange
|
||||
|
|
|
@ -108,3 +108,9 @@ function getRolesForUser(roles, user) {
|
|||
|
||||
return buildRoles(userRoles)
|
||||
}
|
||||
|
||||
export const buildYLabel = queryConfig => {
|
||||
return queryConfig.rawText
|
||||
? ''
|
||||
: `${queryConfig.measurement}.${queryConfig.fields[0].field}`
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import {errorThrown} from 'shared/actions/errors'
|
|||
|
||||
import * as actionTypes from 'src/status/constants/actionTypes'
|
||||
|
||||
import {HTTP_NOT_FOUND} from 'shared/constants'
|
||||
|
||||
const fetchJSONFeedRequested = () => ({
|
||||
type: actionTypes.FETCH_JSON_FEED_REQUESTED,
|
||||
})
|
||||
|
@ -45,15 +43,11 @@ export const fetchJSONFeedAsync = url => async dispatch => {
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(fetchJSONFeedFailed())
|
||||
if (error.status === HTTP_NOT_FOUND) {
|
||||
dispatch(
|
||||
errorThrown(
|
||||
error,
|
||||
`Failed to fetch News Feed. JSON Feed at '${url}' returned 404 (Not Found)`
|
||||
)
|
||||
dispatch(
|
||||
errorThrown(
|
||||
error,
|
||||
`Failed to fetch JSON Feed for News Feed from '${url}'`
|
||||
)
|
||||
} else {
|
||||
dispatch(errorThrown(error, 'Failed to fetch NewsFeed'))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
@import 'layout/flash-messages';
|
||||
|
||||
// Components
|
||||
@import 'components/ceo-display-options';
|
||||
@import 'components/confirm-buttons';
|
||||
@import 'components/custom-time-range';
|
||||
@import 'components/dygraphs';
|
||||
|
@ -47,7 +48,6 @@
|
|||
@import 'components/tables';
|
||||
|
||||
// Pages
|
||||
|
||||
@import 'pages/config-endpoints';
|
||||
@import 'pages/signup';
|
||||
@import 'pages/auth-page';
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
$write-data--max-width: 960px;
|
||||
$write-data--gutter: 30px;
|
||||
$write-data--margin: 18px;
|
||||
$write-data--input-height: 120px;
|
||||
$write-data--input-height: calc(90vh - 48px - 60px - 36px); // Heights of everything but input height
|
||||
$write-data--transition: opacity 0.4s ease;
|
||||
|
||||
.write-data-form {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
$overlay-controls-height: 50px;
|
||||
$overlay-controls-height: 60px;
|
||||
$overlay-controls-bg: $g2-kevlar;
|
||||
$overlay-z: 100;
|
||||
|
||||
|
@ -33,17 +33,16 @@ $overlay-z: 100;
|
|||
}
|
||||
|
||||
.overlay-controls {
|
||||
padding: 0 16px;
|
||||
padding: 0 18px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 $overlay-controls-height;
|
||||
width: calc(100% - #{($page-wrapper-padding * 2)});
|
||||
left: $page-wrapper-padding;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
border: 0;
|
||||
background-color: $g2-kevlar;
|
||||
border-radius: $radius $radius 0 0;
|
||||
}
|
||||
.overlay-controls--right {
|
||||
display: flex;
|
||||
|
@ -74,7 +73,9 @@ $overlay-z: 100;
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.overlay-technology--editor .query-maker--empty {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.overlay-controls .confirm-buttons {
|
||||
margin-left: 32px;
|
||||
|
@ -86,8 +87,8 @@ $overlay-z: 100;
|
|||
}
|
||||
.overlay-technology .query-maker {
|
||||
flex: 1 0 0%;
|
||||
padding: 0 8px;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
padding: 0 18px;
|
||||
margin: 0;
|
||||
background-color: $g2-kevlar;
|
||||
}
|
||||
.overlay-technology .query-maker--tabs {
|
||||
|
|
Loading…
Reference in New Issue