Merge branch 'master' into multiple-event-handlers
commit
903e461d40
canned
influx
ui
spec
data_explorer/reducers
kapacitor/reducers
shared
presenters
query
src
dashboards
constants
containers
graphics
data_explorer
actions/view
constants
containers
reducers
hosts
kapacitor
actions
apis
components
containers
reducers
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,5 +1,7 @@
|
|||
## v1.3.11.0 [unreleased]
|
||||
### Bug Fixes
|
||||
1. [#2449](https://github.com/influxdata/chronograf/pull/2449): Fix .jsdep step fails when LDFLAGS is exported
|
||||
1. [#2157](https://github.com/influxdata/chronograf/pull/2157): Fix logscale producing console errors when only one point in graph
|
||||
1. [#2157](https://github.com/influxdata/chronograf/pull/2157): Fix logscale producing console errors when only one point in graph
|
||||
1. [#2158](https://github.com/influxdata/chronograf/pull/2158): Fix 'Cannot connect to source' false error flag on Dashboard page
|
||||
1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export
|
||||
|
@ -8,9 +10,34 @@
|
|||
1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics.
|
||||
1. [#2303](https://github.com/influxdata/chronograf/pull/2303): Add shadow-utils to RPM release packages
|
||||
1. [#2292](https://github.com/influxdata/chronograf/pull/2292): Source extra command line options from defaults file
|
||||
1. [#2327](https://github.com/influxdata/chronograf/pull/2327): After CREATE/DELETE queries, refresh list of databases in Data Explorer
|
||||
1. [#2327](https://github.com/influxdata/chronograf/pull/2327): Visualize CREATE/DELETE queries with Table view in Data Explorer
|
||||
1. [#2329](https://github.com/influxdata/chronograf/pull/2329): Include tag values alongside measurement name in Data Explorer result tabs
|
||||
1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Redesign cell display options panel
|
||||
1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Introduce customizable Gauge visualization type for dashboard cells
|
||||
1. [#2386](https://github.com/influxdata/chronograf/pull/2386): Fix queries that include regex, numbers and wildcard
|
||||
1. [#2398](https://github.com/influxdata/chronograf/pull/2398): Fix apps on hosts page from parsing tags with null values
|
||||
1. [#2408](https://github.com/influxdata/chronograf/pull/2408): Fix updated Dashboard names not updating dashboard list
|
||||
1. [#2444](https://github.com/influxdata/chronograf/pull/2444): Fix create dashboard button
|
||||
1. [#2416](https://github.com/influxdata/chronograf/pull/2416): Fix default y-axis labels not displaying properly
|
||||
1. [#2423](https://github.com/influxdata/chronograf/pull/2423): Gracefully scale Template Variables Manager overlay on smaller displays
|
||||
1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Fix Influx Enterprise users from deletion in race condition
|
||||
1. [#2467](https://github.com/influxdata/chronograf/pull/2467): Fix oauth2 logout link not having basepath
|
||||
1. [#2466](https://github.com/influxdata/chronograf/pull/2466): Fix supplying a role link to sources that do not have a metaURL
|
||||
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Fix hoverline intermittently not rendering
|
||||
1. [#2483](https://github.com/influxdata/chronograf/pull/2483): Update MySQL pre-canned dashboard to have query derivative correctly
|
||||
|
||||
### Features
|
||||
1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
|
||||
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
|
||||
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
|
||||
1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
|
||||
1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Add auto group by time to Data Explorer
|
||||
1. [#2456](https://github.com/influxdata/chronograf/pull/2456): Add boolean thresholds for kapacitor threshold alerts
|
||||
1. [#2460](https://github.com/influxdata/chronograf/pull/2460): Update kapacitor alerts to cast to float before sending to influx
|
||||
1. [#2479](https://github.com/influxdata/chronograf/pull/2479): Support authentication for Enterprise Meta Nodes
|
||||
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering
|
||||
|
||||
### UI Improvements
|
||||
|
||||
## v1.3.10.0 [2017-10-24]
|
||||
|
@ -33,7 +60,7 @@
|
|||
### UI Improvements
|
||||
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings
|
||||
1. [#2120](https://github.com/influxdata/chronograf/pull/2120): Improve appearance of Admin Page tabs on smaller screens
|
||||
1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to Tickscript editor
|
||||
1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to TICKscript editor
|
||||
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard naming & renaming interaction
|
||||
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard switching dropdown
|
||||
|
||||
|
@ -53,7 +80,7 @@
|
|||
|
||||
### Features
|
||||
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
|
||||
1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKScript
|
||||
1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKscript
|
||||
1. [#1721](https://github.com/influxdata/chronograf/pull/1721): Introduce the TICKscript editor UI
|
||||
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
|
||||
1. [#2082](https://github.com/influxdata/chronograf/pull/2082): Add Data Explorer InfluxQL query and location query synchronization, so queries can be shared via a a URL
|
||||
|
|
26
Makefile
26
Makefile
|
@ -8,6 +8,7 @@ YARN := $(shell command -v yarn 2> /dev/null)
|
|||
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go' -not -path "./vendor/*" )
|
||||
UISOURCES := $(shell find ui -type f -not \( -path ui/build/\* -o -path ui/node_modules/\* -prune \) )
|
||||
|
||||
unexport LDFLAGS
|
||||
LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.commit=${COMMIT}"
|
||||
BINARY=chronograf
|
||||
|
||||
|
@ -23,23 +24,14 @@ ${BINARY}: $(SOURCES) .bindata .jsdep .godep
|
|||
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
|
||||
|
||||
define CHRONOGIRAFFE
|
||||
.-. .-.
|
||||
| \/ |
|
||||
/, ,_ `'-.
|
||||
.-|\ /`\ '.
|
||||
.' 0/ | 0\ \_ `".
|
||||
.-' _,/ '--'.'|#''---'
|
||||
`--' | / \#
|
||||
| / \#
|
||||
\ ;|\ .\#
|
||||
|' ' // \ ::\#
|
||||
\ /` \ ':\#
|
||||
`"` \.. \#
|
||||
\::. \#
|
||||
\:: \#
|
||||
\' .:\#
|
||||
\ :::\#
|
||||
\ '::\#
|
||||
._ o o
|
||||
\_`-)|_
|
||||
,"" _\_
|
||||
," ## | 0 0.
|
||||
," ## ,-\__ `.
|
||||
," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!"
|
||||
," ## /
|
||||
," ## /
|
||||
endef
|
||||
export CHRONOGIRAFFE
|
||||
chronogiraffe: ${BINARY}
|
||||
|
|
127
README.md
127
README.md
|
@ -1,6 +1,8 @@
|
|||
# Chronograf
|
||||
|
||||
Chronograf is an open-source web application written in Go and React.js that provides the tools to visualize your monitoring data and easily create alerting and automation rules.
|
||||
Chronograf is an open-source web application written in Go and React.js that
|
||||
provides the tools to visualize your monitoring data and easily create alerting
|
||||
and automation rules.
|
||||
|
||||
<p align="left">
|
||||
<img src="https://github.com/influxdata/chronograf/blob/master/docs/images/overview-readme.png"/>
|
||||
|
@ -16,8 +18,11 @@ Chronograf is an open-source web application written in Go and React.js that pro
|
|||
|
||||
### Dashboard Templates
|
||||
|
||||
Chronograf's [pre-canned dashboards](https://github.com/influxdata/chronograf/tree/master/canned) for the supported [Telegraf](https://github.com/influxdata/telegraf) input plugins.
|
||||
Currently, Chronograf offers dashboard templates for the following Telegraf input plugins:
|
||||
Chronograf's
|
||||
[pre-canned dashboards](https://github.com/influxdata/chronograf/tree/master/canned)
|
||||
for the supported [Telegraf](https://github.com/influxdata/telegraf) input
|
||||
plugins. Currently, Chronograf offers dashboard templates for the following
|
||||
Telegraf input plugins:
|
||||
|
||||
* [Apache](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/apache)
|
||||
* [Consul](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/consul)
|
||||
|
@ -43,40 +48,49 @@ Currently, Chronograf offers dashboard templates for the following Telegraf inpu
|
|||
* [Redis](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/redis)
|
||||
* [Riak](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/riak)
|
||||
* [System](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/SYSTEM_README.md)
|
||||
* [CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md)
|
||||
* [Disk](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/DISK_README.md)
|
||||
* [DiskIO](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/disk.go#L136)
|
||||
* [Memory](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/MEM_README.md)
|
||||
* [Net](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/net.go)
|
||||
* [Netstat](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/NETSTAT_README.md)
|
||||
* [Processes](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/PROCESSES_README.md)
|
||||
* [Procstat](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/procstat/README.md)
|
||||
* [CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md)
|
||||
* [Disk](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/DISK_README.md)
|
||||
* [DiskIO](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/disk.go#L136)
|
||||
* [Memory](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/MEM_README.md)
|
||||
* [Net](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/net.go)
|
||||
* [Netstat](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/NETSTAT_README.md)
|
||||
* [Processes](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/PROCESSES_README.md)
|
||||
* [Procstat](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/procstat/README.md)
|
||||
* [Varnish](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/varnish)
|
||||
* [Windows Performance Counters](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/win_perf_counters)
|
||||
|
||||
> Note: If a `telegraf` instance isn't running the `system` and `cpu` plugins the canned dashboards from that instance won't be generated.
|
||||
> Note: If a `telegraf` instance isn't running the `system` and `cpu` plugins
|
||||
> the canned dashboards from that instance won't be generated.
|
||||
|
||||
### Data Explorer
|
||||
|
||||
Chronograf's graphing tool that allows you to dig in and create personalized visualizations of your data.
|
||||
Chronograf's graphing tool that allows you to dig in and create personalized
|
||||
visualizations of your data.
|
||||
|
||||
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the query editor
|
||||
* Generate and edit
|
||||
[InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/)
|
||||
statements with the query editor
|
||||
* Use Chronograf's query templates to easily explore your data
|
||||
* Create visualizations and view query results in tabular format
|
||||
|
||||
### Dashboards
|
||||
|
||||
Create and edit customized dashboards. The dashboards support several visualization types including line graphs, stacked graphs, step plots, single statistic graphs, and line-single-statistic graphs.
|
||||
Create and edit customized dashboards. The dashboards support several
|
||||
visualization types including line graphs, stacked graphs, step plots, single
|
||||
statistic graphs, and line-single-statistic graphs.
|
||||
|
||||
Use Chronograf's template variables to easily adjust the data that appear in your graphs and gain deeper insight into your data.
|
||||
Use Chronograf's template variables to easily adjust the data that appear in
|
||||
your graphs and gain deeper insight into your data.
|
||||
|
||||
### Kapacitor UI
|
||||
|
||||
A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and alert tracking.
|
||||
A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and
|
||||
alert tracking.
|
||||
|
||||
* Simply generate threshold, relative, and deadman alerts
|
||||
* Preview data and alert boundaries while creating an alert
|
||||
* Configure alert destinations - Currently, Chronograf supports sending alerts to:
|
||||
* Configure alert destinations - Currently, Chronograf supports sending alerts
|
||||
to:
|
||||
* [Alerta](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#alerta)
|
||||
* [Exec](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#exec)
|
||||
* [HipChat](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#hipchat)
|
||||
|
@ -96,45 +110,71 @@ A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and
|
|||
|
||||
### User and Query Management
|
||||
|
||||
Manage users, roles, permissions for [OSS InfluxDB](https://github.com/influxdata/influxdb) and InfluxData's [Enterprise](https://docs.influxdata.com/enterprise/v1.2/) product.
|
||||
View actively running queries and stop expensive queries on the Query Management page.
|
||||
Manage users, roles, permissions for
|
||||
[OSS InfluxDB](https://github.com/influxdata/influxdb) and InfluxData's
|
||||
[Enterprise](https://docs.influxdata.com/enterprise/v1.2/) product. View
|
||||
actively running queries and stop expensive queries on the Query Management
|
||||
page.
|
||||
|
||||
### TLS/HTTPS Support
|
||||
See [Chronograf with TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) for more information.
|
||||
|
||||
See
|
||||
[Chronograf with TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md)
|
||||
for more information.
|
||||
|
||||
### OAuth Login
|
||||
See [Chronograf with OAuth 2.0](https://github.com/influxdata/chronograf/blob/master/docs/auth.md) for more information.
|
||||
|
||||
See
|
||||
[Chronograf with OAuth 2.0](https://github.com/influxdata/chronograf/blob/master/docs/auth.md)
|
||||
for more information.
|
||||
|
||||
### Advanced Routing
|
||||
Change the default root path of the Chronograf server with the `--basepath` option.
|
||||
|
||||
Change the default root path of the Chronograf server with the `--basepath`
|
||||
option.
|
||||
|
||||
## Versions
|
||||
|
||||
The most recent version of Chronograf is [v1.3.10.0](https://www.influxdata.com/downloads/).
|
||||
The most recent version of Chronograf is
|
||||
[v1.3.10.0](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request?
|
||||
Please open [an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
Spotted a bug or have a feature request? Please open
|
||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
||||
### Known Issues
|
||||
|
||||
The Chronograf team has identified and is working on the following issues:
|
||||
|
||||
* Chronograf requires users to run Telegraf's [CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md) and [system](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/SYSTEM_README.md) plugins to ensure that all Apps appear on the [HOST LIST](https://github.com/influxdata/chronograf/blob/master/docs/GETTING_STARTED.md#host-list) page.
|
||||
* Chronograf requires users to run Telegraf's
|
||||
[CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md)
|
||||
and
|
||||
[system](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/SYSTEM_README.md)
|
||||
plugins to ensure that all Apps appear on the
|
||||
[HOST LIST](https://github.com/influxdata/chronograf/blob/master/docs/GETTING_STARTED.md#host-list)
|
||||
page.
|
||||
|
||||
## Installation
|
||||
|
||||
Check out the [INSTALLATION](https://docs.influxdata.com/chronograf/v1.3/introduction/installation/) guide to get up and running with Chronograf with as little configuration and code as possible.
|
||||
Check out the
|
||||
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.3/introduction/installation/)
|
||||
guide to get up and running with Chronograf with as little configuration and
|
||||
code as possible.
|
||||
|
||||
We recommend installing Chronograf using one of the [pre-built packages](https://influxdata.com/downloads/#chronograf). Then start Chronograf using:
|
||||
We recommend installing Chronograf using one of the
|
||||
[pre-built packages](https://influxdata.com/downloads/#chronograf). Then start
|
||||
Chronograf using:
|
||||
|
||||
* `service chronograf start` if you have installed Chronograf using an official Debian or RPM package.
|
||||
* `systemctl start chronograf` if you have installed Chronograf using an official Debian or RPM package, and are running a distro with `systemd`. For example, Ubuntu 15 or later.
|
||||
* `service chronograf start` if you have installed Chronograf using an official
|
||||
Debian or RPM package.
|
||||
* `systemctl start chronograf` if you have installed Chronograf using an
|
||||
official Debian or RPM package, and are running a distro with `systemd`. For
|
||||
example, Ubuntu 15 or later.
|
||||
* `$GOPATH/bin/chronograf` if you have built Chronograf from source.
|
||||
|
||||
By default, chronograf runs on port `8888`.
|
||||
|
||||
|
||||
### With Docker
|
||||
|
||||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
|
@ -144,7 +184,8 @@ docker pull chronograf:1.3.10.0
|
|||
### From Source
|
||||
|
||||
* Chronograf works with go 1.8.x, node 6.x/7.x, and yarn 0.18+.
|
||||
* Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor) 1.2.x+ to create and store alerts.
|
||||
* Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor)
|
||||
1.2.x+ to create and store alerts.
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install)
|
||||
1. [Install Node and NPM](https://nodejs.org/en/download/)
|
||||
|
@ -157,11 +198,23 @@ docker pull chronograf:1.3.10.0
|
|||
|
||||
## Documentation
|
||||
|
||||
[Getting Started](https://docs.influxdata.com/chronograf/v1.3/introduction/getting-started/) will get you up and running with Chronograf with as little configuration and code as possible.
|
||||
See our [guides](https://docs.influxdata.com/chronograf/v1.3/guides/) to get familiar with Chronograf's main features.
|
||||
[Getting Started](https://docs.influxdata.com/chronograf/v1.3/introduction/getting-started/)
|
||||
will get you up and running with Chronograf with as little configuration and
|
||||
code as possible. See our
|
||||
[guides](https://docs.influxdata.com/chronograf/v1.3/guides/) to get familiar
|
||||
with Chronograf's main features.
|
||||
|
||||
Documentation for Telegraf, InfluxDB, and Kapacitor are available at https://docs.influxdata.com/.
|
||||
Documentation for Telegraf, InfluxDB, and Kapacitor are available at
|
||||
https://docs.influxdata.com/.
|
||||
|
||||
Chronograf uses
|
||||
[swagger](https://swagger.io/specification://swagger.io/specification/) to
|
||||
document its REST interfaces. To reach the documentation, run the server and go
|
||||
to the `/docs` for example at http://localhost:8888/docs
|
||||
|
||||
The swagger JSON document is in `server/swagger.json`
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see the [contributing guide](CONTRIBUTING.md) for details on contributing to Chronograf.
|
||||
Please see the [contributing guide](CONTRIBUTING.md) for details on contributing
|
||||
to Chronograf.
|
||||
|
|
|
@ -86,6 +86,7 @@ func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (ch
|
|||
id, _ := b.NextSequence()
|
||||
|
||||
src.ID = chronograf.DashboardID(id)
|
||||
// TODO: use FormatInt
|
||||
strID := strconv.Itoa(int(id))
|
||||
for i, cell := range src.Cells {
|
||||
cid, err := d.IDs.Generate()
|
||||
|
@ -95,12 +96,11 @@ func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (ch
|
|||
cell.ID = cid
|
||||
src.Cells[i] = cell
|
||||
}
|
||||
if v, err := internal.MarshalDashboard(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put([]byte(strID), v); err != nil {
|
||||
v, err := internal.MarshalDashboard(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return b.Put([]byte(strID), v)
|
||||
}); err != nil {
|
||||
return chronograf.Dashboard{}, err
|
||||
}
|
||||
|
|
|
@ -191,12 +191,37 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
if q.Range != nil {
|
||||
r.Upper, r.Lower = q.Range.Upper, q.Range.Lower
|
||||
}
|
||||
q.Shifts = q.QueryConfig.Shifts
|
||||
queries[j] = &Query{
|
||||
Command: q.Command,
|
||||
Label: q.Label,
|
||||
Range: r,
|
||||
Source: q.Source,
|
||||
}
|
||||
|
||||
shifts := make([]*TimeShift, len(q.Shifts))
|
||||
for k := range q.Shifts {
|
||||
shift := &TimeShift{
|
||||
Label: q.Shifts[k].Label,
|
||||
Unit: q.Shifts[k].Unit,
|
||||
Quantity: q.Shifts[k].Quantity,
|
||||
}
|
||||
|
||||
shifts[k] = shift
|
||||
}
|
||||
|
||||
queries[j].Shifts = shifts
|
||||
}
|
||||
|
||||
colors := make([]*Color, len(c.CellColors))
|
||||
for j, color := range c.CellColors {
|
||||
colors[j] = &Color{
|
||||
ID: color.ID,
|
||||
Type: color.Type,
|
||||
Hex: color.Hex,
|
||||
Name: color.Name,
|
||||
Value: color.Value,
|
||||
}
|
||||
}
|
||||
|
||||
axes := make(map[string]*Axis, len(c.Axes))
|
||||
|
@ -221,6 +246,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
Queries: queries,
|
||||
Type: c.Type,
|
||||
Axes: axes,
|
||||
Colors: colors,
|
||||
}
|
||||
}
|
||||
templates := make([]*Template, len(d.Templates))
|
||||
|
@ -277,12 +303,37 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
Label: q.Label,
|
||||
Source: q.Source,
|
||||
}
|
||||
|
||||
if q.Range.Upper != q.Range.Lower {
|
||||
queries[j].Range = &chronograf.Range{
|
||||
Upper: q.Range.Upper,
|
||||
Lower: q.Range.Lower,
|
||||
}
|
||||
}
|
||||
|
||||
shifts := make([]chronograf.TimeShift, len(q.Shifts))
|
||||
for k := range q.Shifts {
|
||||
shift := chronograf.TimeShift{
|
||||
Label: q.Shifts[k].Label,
|
||||
Unit: q.Shifts[k].Unit,
|
||||
Quantity: q.Shifts[k].Quantity,
|
||||
}
|
||||
|
||||
shifts[k] = shift
|
||||
}
|
||||
|
||||
queries[j].Shifts = shifts
|
||||
}
|
||||
|
||||
colors := make([]chronograf.CellColor, len(c.Colors))
|
||||
for j, color := range c.Colors {
|
||||
colors[j] = chronograf.CellColor{
|
||||
ID: color.ID,
|
||||
Type: color.Type,
|
||||
Hex: color.Hex,
|
||||
Name: color.Name,
|
||||
Value: color.Value,
|
||||
}
|
||||
}
|
||||
|
||||
axes := make(map[string]chronograf.Axis, len(c.Axes))
|
||||
|
@ -316,15 +367,16 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
}
|
||||
|
||||
cells[i] = chronograf.DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
Y: c.Y,
|
||||
W: c.W,
|
||||
H: c.H,
|
||||
Name: c.Name,
|
||||
Queries: queries,
|
||||
Type: c.Type,
|
||||
Axes: axes,
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
Y: c.Y,
|
||||
W: c.W,
|
||||
H: c.H,
|
||||
Name: c.Name,
|
||||
Queries: queries,
|
||||
Type: c.Type,
|
||||
Axes: axes,
|
||||
CellColors: colors,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ It has these top-level messages:
|
|||
Source
|
||||
Dashboard
|
||||
DashboardCell
|
||||
Color
|
||||
Axis
|
||||
Template
|
||||
TemplateValue
|
||||
|
@ -20,6 +21,7 @@ It has these top-level messages:
|
|||
Layout
|
||||
Cell
|
||||
Query
|
||||
TimeShift
|
||||
Range
|
||||
AlertRule
|
||||
User
|
||||
|
@ -96,6 +98,7 @@ type DashboardCell struct {
|
|||
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
|
||||
ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Axes map[string]*Axis `protobuf:"bytes,9,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Colors []*Color `protobuf:"bytes,10,rep,name=colors" json:"colors,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
|
||||
|
@ -117,6 +120,26 @@ func (m *DashboardCell) GetAxes() map[string]*Axis {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *DashboardCell) GetColors() []*Color {
|
||||
if m != nil {
|
||||
return m.Colors
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Color struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"`
|
||||
Hex string `protobuf:"bytes,3,opt,name=Hex,proto3" json:"Hex,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Value string `protobuf:"bytes,5,opt,name=Value,proto3" json:"Value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Color) Reset() { *m = Color{} }
|
||||
func (m *Color) String() string { return proto.CompactTextString(m) }
|
||||
func (*Color) ProtoMessage() {}
|
||||
func (*Color) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
||||
|
||||
type Axis struct {
|
||||
LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"`
|
||||
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
|
||||
|
@ -130,7 +153,7 @@ type Axis struct {
|
|||
func (m *Axis) Reset() { *m = Axis{} }
|
||||
func (m *Axis) String() string { return proto.CompactTextString(m) }
|
||||
func (*Axis) ProtoMessage() {}
|
||||
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
||||
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
|
||||
type Template struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -144,7 +167,7 @@ type Template struct {
|
|||
func (m *Template) Reset() { *m = Template{} }
|
||||
func (m *Template) String() string { return proto.CompactTextString(m) }
|
||||
func (*Template) ProtoMessage() {}
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
func (m *Template) GetValues() []*TemplateValue {
|
||||
if m != nil {
|
||||
|
@ -169,7 +192,7 @@ type TemplateValue struct {
|
|||
func (m *TemplateValue) Reset() { *m = TemplateValue{} }
|
||||
func (m *TemplateValue) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateValue) ProtoMessage() {}
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
|
||||
type TemplateQuery struct {
|
||||
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
|
||||
|
@ -183,7 +206,7 @@ type TemplateQuery struct {
|
|||
func (m *TemplateQuery) Reset() { *m = TemplateQuery{} }
|
||||
func (m *TemplateQuery) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateQuery) ProtoMessage() {}
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
|
||||
type Server struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -198,7 +221,7 @@ type Server struct {
|
|||
func (m *Server) Reset() { *m = Server{} }
|
||||
func (m *Server) String() string { return proto.CompactTextString(m) }
|
||||
func (*Server) ProtoMessage() {}
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
|
||||
type Layout struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -211,7 +234,7 @@ type Layout struct {
|
|||
func (m *Layout) Reset() { *m = Layout{} }
|
||||
func (m *Layout) String() string { return proto.CompactTextString(m) }
|
||||
func (*Layout) ProtoMessage() {}
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
|
||||
func (m *Layout) GetCells() []*Cell {
|
||||
if m != nil {
|
||||
|
@ -237,7 +260,7 @@ type Cell struct {
|
|||
func (m *Cell) Reset() { *m = Cell{} }
|
||||
func (m *Cell) String() string { return proto.CompactTextString(m) }
|
||||
func (*Cell) ProtoMessage() {}
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
|
||||
func (m *Cell) GetQueries() []*Query {
|
||||
if m != nil {
|
||||
|
@ -254,20 +277,21 @@ func (m *Cell) GetAxes() map[string]*Axis {
|
|||
}
|
||||
|
||||
type Query struct {
|
||||
Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"`
|
||||
DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"`
|
||||
RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"`
|
||||
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"`
|
||||
Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"`
|
||||
Label string `protobuf:"bytes,6,opt,name=Label,proto3" json:"Label,omitempty"`
|
||||
Range *Range `protobuf:"bytes,7,opt,name=Range" json:"Range,omitempty"`
|
||||
Source string `protobuf:"bytes,8,opt,name=Source,proto3" json:"Source,omitempty"`
|
||||
Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"`
|
||||
DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"`
|
||||
RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"`
|
||||
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"`
|
||||
Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"`
|
||||
Label string `protobuf:"bytes,6,opt,name=Label,proto3" json:"Label,omitempty"`
|
||||
Range *Range `protobuf:"bytes,7,opt,name=Range" json:"Range,omitempty"`
|
||||
Source string `protobuf:"bytes,8,opt,name=Source,proto3" json:"Source,omitempty"`
|
||||
Shifts []*TimeShift `protobuf:"bytes,9,rep,name=Shifts" json:"Shifts,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Query) Reset() { *m = Query{} }
|
||||
func (m *Query) String() string { return proto.CompactTextString(m) }
|
||||
func (*Query) ProtoMessage() {}
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
|
||||
func (m *Query) GetRange() *Range {
|
||||
if m != nil {
|
||||
|
@ -276,6 +300,24 @@ func (m *Query) GetRange() *Range {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Query) GetShifts() []*TimeShift {
|
||||
if m != nil {
|
||||
return m.Shifts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TimeShift struct {
|
||||
Label string `protobuf:"bytes,1,opt,name=Label,proto3" json:"Label,omitempty"`
|
||||
Unit string `protobuf:"bytes,2,opt,name=Unit,proto3" json:"Unit,omitempty"`
|
||||
Quantity string `protobuf:"bytes,3,opt,name=Quantity,proto3" json:"Quantity,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TimeShift) Reset() { *m = TimeShift{} }
|
||||
func (m *TimeShift) String() string { return proto.CompactTextString(m) }
|
||||
func (*TimeShift) ProtoMessage() {}
|
||||
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
|
||||
type Range struct {
|
||||
Upper int64 `protobuf:"varint,1,opt,name=Upper,proto3" json:"Upper,omitempty"`
|
||||
Lower int64 `protobuf:"varint,2,opt,name=Lower,proto3" json:"Lower,omitempty"`
|
||||
|
@ -284,7 +326,7 @@ type Range struct {
|
|||
func (m *Range) Reset() { *m = Range{} }
|
||||
func (m *Range) String() string { return proto.CompactTextString(m) }
|
||||
func (*Range) ProtoMessage() {}
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
|
||||
|
||||
type AlertRule struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -296,7 +338,7 @@ type AlertRule struct {
|
|||
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
||||
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
|
||||
func (*AlertRule) ProtoMessage() {}
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
|
||||
|
||||
type User struct {
|
||||
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -306,12 +348,13 @@ type User struct {
|
|||
func (m *User) Reset() { *m = User{} }
|
||||
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||
func (*User) ProtoMessage() {}
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
||||
proto.RegisterType((*Color)(nil), "internal.Color")
|
||||
proto.RegisterType((*Axis)(nil), "internal.Axis")
|
||||
proto.RegisterType((*Template)(nil), "internal.Template")
|
||||
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
|
||||
|
@ -320,6 +363,7 @@ func init() {
|
|||
proto.RegisterType((*Layout)(nil), "internal.Layout")
|
||||
proto.RegisterType((*Cell)(nil), "internal.Cell")
|
||||
proto.RegisterType((*Query)(nil), "internal.Query")
|
||||
proto.RegisterType((*TimeShift)(nil), "internal.TimeShift")
|
||||
proto.RegisterType((*Range)(nil), "internal.Range")
|
||||
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
|
||||
proto.RegisterType((*User)(nil), "internal.User")
|
||||
|
@ -328,70 +372,76 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 1028 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0x4f, 0x6f, 0xe3, 0x44,
|
||||
0x14, 0xd7, 0xf8, 0x4f, 0x12, 0xbf, 0x74, 0x0b, 0x1a, 0xad, 0x58, 0xb3, 0x5c, 0x82, 0x05, 0x52,
|
||||
0x40, 0x6c, 0x41, 0xbb, 0x42, 0x42, 0xdc, 0xd2, 0x06, 0xad, 0x4a, 0xbb, 0x4b, 0x99, 0xb4, 0xe5,
|
||||
0x84, 0x56, 0x13, 0xe7, 0xa5, 0xb5, 0xd6, 0x89, 0xcd, 0xd8, 0x6e, 0xe3, 0x6f, 0xc1, 0x27, 0x40,
|
||||
0x42, 0xe2, 0xc4, 0x81, 0x03, 0x5f, 0x80, 0xfb, 0x7e, 0x2a, 0xf4, 0x66, 0xc6, 0x8e, 0xc3, 0x76,
|
||||
0xd1, 0x5e, 0xe0, 0x36, 0xbf, 0xf7, 0xc6, 0x6f, 0x66, 0xde, 0xef, 0xfd, 0x7e, 0x09, 0xec, 0x27,
|
||||
0xeb, 0x12, 0xd5, 0x5a, 0xa6, 0x07, 0xb9, 0xca, 0xca, 0x8c, 0x0f, 0x1a, 0x1c, 0xfd, 0xe1, 0x40,
|
||||
0x6f, 0x96, 0x55, 0x2a, 0x46, 0xbe, 0x0f, 0xce, 0xf1, 0x34, 0x64, 0x23, 0x36, 0x76, 0x85, 0x73,
|
||||
0x3c, 0xe5, 0x1c, 0xbc, 0xe7, 0x72, 0x85, 0xa1, 0x33, 0x62, 0xe3, 0x40, 0xe8, 0x35, 0xc5, 0xce,
|
||||
0xeb, 0x1c, 0x43, 0xd7, 0xc4, 0x68, 0xcd, 0x1f, 0xc2, 0xe0, 0xa2, 0xa0, 0x6a, 0x2b, 0x0c, 0x3d,
|
||||
0x1d, 0x6f, 0x31, 0xe5, 0xce, 0x64, 0x51, 0xdc, 0x66, 0x6a, 0x11, 0xfa, 0x26, 0xd7, 0x60, 0xfe,
|
||||
0x2e, 0xb8, 0x17, 0xe2, 0x34, 0xec, 0xe9, 0x30, 0x2d, 0x79, 0x08, 0xfd, 0x29, 0x2e, 0x65, 0x95,
|
||||
0x96, 0x61, 0x7f, 0xc4, 0xc6, 0x03, 0xd1, 0x40, 0xaa, 0x73, 0x8e, 0x29, 0x5e, 0x29, 0xb9, 0x0c,
|
||||
0x07, 0xa6, 0x4e, 0x83, 0xf9, 0x01, 0xf0, 0xe3, 0x75, 0x81, 0x71, 0xa5, 0x70, 0xf6, 0x32, 0xc9,
|
||||
0x2f, 0x51, 0x25, 0xcb, 0x3a, 0x0c, 0x74, 0x81, 0x3b, 0x32, 0x74, 0xca, 0x33, 0x2c, 0x25, 0x9d,
|
||||
0x0d, 0xba, 0x54, 0x03, 0x79, 0x04, 0x7b, 0xb3, 0x6b, 0xa9, 0x70, 0x31, 0xc3, 0x58, 0x61, 0x19,
|
||||
0x0e, 0x75, 0x7a, 0x27, 0x16, 0xfd, 0xcc, 0x20, 0x98, 0xca, 0xe2, 0x7a, 0x9e, 0x49, 0xb5, 0x78,
|
||||
0xab, 0x9e, 0x3d, 0x02, 0x3f, 0xc6, 0x34, 0x2d, 0x42, 0x77, 0xe4, 0x8e, 0x87, 0x8f, 0x1f, 0x1c,
|
||||
0xb4, 0x64, 0xb4, 0x75, 0x8e, 0x30, 0x4d, 0x85, 0xd9, 0xc5, 0xbf, 0x80, 0xa0, 0xc4, 0x55, 0x9e,
|
||||
0xca, 0x12, 0x8b, 0xd0, 0xd3, 0x9f, 0xf0, 0xed, 0x27, 0xe7, 0x36, 0x25, 0xb6, 0x9b, 0xa2, 0xdf,
|
||||
0x1d, 0xb8, 0xb7, 0x53, 0x8a, 0xef, 0x01, 0xdb, 0xe8, 0x5b, 0xf9, 0x82, 0x6d, 0x08, 0xd5, 0xfa,
|
||||
0x46, 0xbe, 0x60, 0x35, 0xa1, 0x5b, 0xcd, 0x9f, 0x2f, 0xd8, 0x2d, 0xa1, 0x6b, 0xcd, 0x9a, 0x2f,
|
||||
0xd8, 0x35, 0xff, 0x04, 0xfa, 0x3f, 0x55, 0xa8, 0x12, 0x2c, 0x42, 0x5f, 0x9f, 0xfc, 0xce, 0xf6,
|
||||
0xe4, 0xef, 0x2b, 0x54, 0xb5, 0x68, 0xf2, 0xf4, 0x52, 0xcd, 0xb8, 0xa1, 0x4f, 0xaf, 0x29, 0x56,
|
||||
0xd2, 0x74, 0xf4, 0x4d, 0x8c, 0xd6, 0xb6, 0x43, 0x86, 0x33, 0xea, 0xd0, 0x97, 0xe0, 0xc9, 0x0d,
|
||||
0x16, 0x61, 0xa0, 0xeb, 0x7f, 0xf8, 0x86, 0x66, 0x1c, 0x4c, 0x36, 0x58, 0x7c, 0xb3, 0x2e, 0x55,
|
||||
0x2d, 0xf4, 0xf6, 0x87, 0x4f, 0x21, 0x68, 0x43, 0x34, 0x39, 0x2f, 0xb1, 0xd6, 0x0f, 0x0c, 0x04,
|
||||
0x2d, 0xf9, 0x47, 0xe0, 0xdf, 0xc8, 0xb4, 0x32, 0x8d, 0x1f, 0x3e, 0xde, 0xdf, 0x96, 0x9d, 0x6c,
|
||||
0x92, 0x42, 0x98, 0xe4, 0xd7, 0xce, 0x57, 0x2c, 0xfa, 0x93, 0x81, 0x47, 0x31, 0x22, 0x3b, 0xc5,
|
||||
0x2b, 0x19, 0xd7, 0x87, 0x59, 0xb5, 0x5e, 0x14, 0x21, 0x1b, 0xb9, 0x63, 0x57, 0xec, 0xc4, 0xf8,
|
||||
0x7b, 0xd0, 0x9b, 0x9b, 0xac, 0x33, 0x72, 0xc7, 0x81, 0xb0, 0x88, 0xdf, 0x07, 0x3f, 0x95, 0x73,
|
||||
0x4c, 0xad, 0x0e, 0x0c, 0xa0, 0xdd, 0xb9, 0xc2, 0x65, 0xb2, 0xb1, 0x32, 0xb0, 0x88, 0xe2, 0x45,
|
||||
0xb5, 0xa4, 0xb8, 0x91, 0x80, 0x45, 0xd4, 0xae, 0xb9, 0x2c, 0xda, 0x16, 0xd2, 0x9a, 0x2a, 0x17,
|
||||
0xb1, 0x4c, 0x9b, 0x1e, 0x1a, 0x10, 0xfd, 0xc5, 0x68, 0xfe, 0x0d, 0xdf, 0x9d, 0x99, 0x33, 0x1d,
|
||||
0x7d, 0x1f, 0x06, 0x34, 0x0b, 0x2f, 0x6e, 0xa4, 0xb2, 0x73, 0xd7, 0x27, 0x7c, 0x29, 0x15, 0xff,
|
||||
0x1c, 0x7a, 0xfa, 0xe5, 0x77, 0xcc, 0x5e, 0x53, 0xee, 0x92, 0xf2, 0xc2, 0x6e, 0x6b, 0x19, 0xf4,
|
||||
0x3a, 0x0c, 0xb6, 0x8f, 0xf5, 0xbb, 0x8f, 0x7d, 0x04, 0x3e, 0x8d, 0x42, 0xad, 0x6f, 0x7f, 0x67,
|
||||
0x65, 0x33, 0x30, 0x66, 0x57, 0x74, 0x01, 0xf7, 0x76, 0x4e, 0x6c, 0x4f, 0x62, 0xbb, 0x27, 0x6d,
|
||||
0x59, 0x0c, 0x2c, 0x6b, 0xa4, 0xfd, 0x02, 0x53, 0x8c, 0x4b, 0x5c, 0xe8, 0x7e, 0x0f, 0x44, 0x8b,
|
||||
0xa3, 0x5f, 0xd9, 0xb6, 0xae, 0x3e, 0x8f, 0xd4, 0x1d, 0x67, 0xab, 0x95, 0x5c, 0x2f, 0x6c, 0xe9,
|
||||
0x06, 0x52, 0xdf, 0x16, 0x73, 0x5b, 0xda, 0x59, 0xcc, 0x09, 0xab, 0xdc, 0x32, 0xe8, 0xa8, 0x9c,
|
||||
0x8f, 0x60, 0xb8, 0x42, 0x59, 0x54, 0x0a, 0x57, 0xb8, 0x2e, 0x6d, 0x0b, 0xba, 0x21, 0xfe, 0x00,
|
||||
0xfa, 0xa5, 0xbc, 0x7a, 0x41, 0xb3, 0x67, 0x99, 0x2c, 0xe5, 0xd5, 0x09, 0xd6, 0xfc, 0x03, 0x08,
|
||||
0x96, 0x09, 0xa6, 0x0b, 0x9d, 0x32, 0x74, 0x0e, 0x74, 0xe0, 0x04, 0xeb, 0xe8, 0x37, 0x06, 0xbd,
|
||||
0x19, 0xaa, 0x1b, 0x54, 0x6f, 0x65, 0x17, 0x5d, 0x3b, 0x75, 0xff, 0xc5, 0x4e, 0xbd, 0xbb, 0xed,
|
||||
0xd4, 0xdf, 0xda, 0xe9, 0x7d, 0xf0, 0x67, 0x2a, 0x3e, 0x9e, 0xea, 0x1b, 0xb9, 0xc2, 0x00, 0x9a,
|
||||
0xc6, 0x49, 0x5c, 0x26, 0x37, 0x68, 0x3d, 0xd6, 0xa2, 0xe8, 0x17, 0x06, 0xbd, 0x53, 0x59, 0x67,
|
||||
0x55, 0xf9, 0xda, 0x84, 0x8d, 0x60, 0x38, 0xc9, 0xf3, 0x34, 0x89, 0x65, 0x99, 0x64, 0x6b, 0x7b,
|
||||
0xdb, 0x6e, 0x88, 0x76, 0x3c, 0xeb, 0xf4, 0xce, 0xdc, 0xbb, 0x1b, 0x22, 0x85, 0x1e, 0x69, 0x17,
|
||||
0x34, 0x96, 0xd6, 0x51, 0xa8, 0x31, 0x3f, 0x9d, 0xa4, 0x07, 0x4e, 0xaa, 0x32, 0x5b, 0xa6, 0xd9,
|
||||
0xad, 0x7e, 0xc9, 0x40, 0xb4, 0x38, 0x7a, 0xe5, 0x80, 0xf7, 0x7f, 0xb9, 0xdb, 0x1e, 0xb0, 0xc4,
|
||||
0x12, 0xc9, 0x92, 0xd6, 0xeb, 0xfa, 0x1d, 0xaf, 0x0b, 0xa1, 0x5f, 0x2b, 0xb9, 0xbe, 0xc2, 0x22,
|
||||
0x1c, 0x68, 0xe7, 0x68, 0xa0, 0xce, 0x68, 0x8d, 0x18, 0x93, 0x0b, 0x44, 0x03, 0xdb, 0x99, 0x87,
|
||||
0xce, 0xcc, 0x7f, 0x66, 0xfd, 0x70, 0xa8, 0x6f, 0x14, 0xee, 0xb6, 0xe5, 0xbf, 0xb3, 0xc1, 0x57,
|
||||
0x0c, 0xfc, 0x56, 0x30, 0x47, 0xbb, 0x82, 0x39, 0xda, 0x0a, 0x66, 0x7a, 0xd8, 0x08, 0x66, 0x7a,
|
||||
0x48, 0x58, 0x9c, 0x35, 0x82, 0x11, 0x67, 0x44, 0xd6, 0x53, 0x95, 0x55, 0xf9, 0x61, 0x6d, 0x58,
|
||||
0x0d, 0x44, 0x8b, 0x69, 0xca, 0x7e, 0xb8, 0x46, 0x65, 0x5b, 0x1d, 0x08, 0x8b, 0x68, 0x26, 0x4f,
|
||||
0xb5, 0x99, 0x98, 0xe6, 0x1a, 0xc0, 0x3f, 0x06, 0x5f, 0x50, 0xf3, 0x74, 0x87, 0x77, 0x78, 0xd1,
|
||||
0x61, 0x61, 0xb2, 0x54, 0xd4, 0xfc, 0x57, 0xb1, 0xbf, 0x27, 0x16, 0x45, 0x4f, 0xec, 0xe7, 0x54,
|
||||
0xfd, 0x22, 0xcf, 0x51, 0x59, 0x89, 0x19, 0xa0, 0xcf, 0xcc, 0x6e, 0xd1, 0xb8, 0xa3, 0x2b, 0x0c,
|
||||
0x88, 0x7e, 0x84, 0x60, 0x92, 0xa2, 0x2a, 0x45, 0x95, 0xbe, 0xee, 0xa9, 0x1c, 0xbc, 0x6f, 0x67,
|
||||
0xdf, 0x3d, 0x6f, 0x84, 0x49, 0xeb, 0xad, 0x9c, 0xdc, 0x7f, 0xc8, 0xe9, 0x44, 0xe6, 0xf2, 0x78,
|
||||
0xaa, 0xe7, 0xcc, 0x15, 0x16, 0x45, 0x9f, 0x82, 0x47, 0xb2, 0xed, 0x54, 0xf6, 0xde, 0x24, 0xf9,
|
||||
0x79, 0x4f, 0xff, 0x2b, 0x7b, 0xf2, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb7, 0x59, 0x2e, 0xc0,
|
||||
0xa7, 0x09, 0x00, 0x00,
|
||||
// 1134 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4,
|
||||
0x13, 0x96, 0x63, 0x3b, 0x89, 0x2b, 0xbb, 0xf3, 0x5b, 0xf5, 0x6f, 0xc5, 0x9a, 0xe5, 0x12, 0x2c,
|
||||
0x10, 0xe1, 0xcf, 0x0e, 0x68, 0x57, 0x48, 0x88, 0x5b, 0x66, 0x82, 0x96, 0x61, 0x66, 0x97, 0x99,
|
||||
0xce, 0xcc, 0x70, 0x42, 0xab, 0x8e, 0x53, 0x49, 0xac, 0x75, 0x6c, 0xd3, 0xb6, 0x67, 0xe2, 0xb7,
|
||||
0xe0, 0x09, 0x90, 0x90, 0x38, 0x73, 0xe0, 0x05, 0xb8, 0x73, 0xe5, 0x61, 0xb8, 0xa2, 0xea, 0x6e,
|
||||
0x3b, 0xce, 0xec, 0x2c, 0xda, 0x03, 0xe2, 0xd6, 0x5f, 0x55, 0xa7, 0xaa, 0xba, 0xea, 0xab, 0x2f,
|
||||
0x86, 0xbd, 0x28, 0x29, 0x50, 0x26, 0x22, 0xde, 0xcf, 0x64, 0x5a, 0xa4, 0xac, 0x5f, 0xe3, 0xe0,
|
||||
0xd7, 0x0e, 0x74, 0xa7, 0x69, 0x29, 0x43, 0x64, 0x7b, 0xd0, 0x39, 0x9a, 0xf8, 0xd6, 0xd0, 0x1a,
|
||||
0xd9, 0xbc, 0x73, 0x34, 0x61, 0x0c, 0x9c, 0xe7, 0x62, 0x8d, 0x7e, 0x67, 0x68, 0x8d, 0x3c, 0xae,
|
||||
0xce, 0x64, 0x3b, 0xaf, 0x32, 0xf4, 0x6d, 0x6d, 0xa3, 0x33, 0x7b, 0x08, 0xfd, 0x8b, 0x9c, 0xa2,
|
||||
0xad, 0xd1, 0x77, 0x94, 0xbd, 0xc1, 0xe4, 0x3b, 0x15, 0x79, 0x7e, 0x9d, 0xca, 0xb9, 0xef, 0x6a,
|
||||
0x5f, 0x8d, 0xd9, 0x3d, 0xb0, 0x2f, 0xf8, 0x89, 0xdf, 0x55, 0x66, 0x3a, 0x32, 0x1f, 0x7a, 0x13,
|
||||
0x5c, 0x88, 0x32, 0x2e, 0xfc, 0xde, 0xd0, 0x1a, 0xf5, 0x79, 0x0d, 0x29, 0xce, 0x39, 0xc6, 0xb8,
|
||||
0x94, 0x62, 0xe1, 0xf7, 0x75, 0x9c, 0x1a, 0xb3, 0x7d, 0x60, 0x47, 0x49, 0x8e, 0x61, 0x29, 0x71,
|
||||
0xfa, 0x32, 0xca, 0x2e, 0x51, 0x46, 0x8b, 0xca, 0xf7, 0x54, 0x80, 0x5b, 0x3c, 0x94, 0xe5, 0x19,
|
||||
0x16, 0x82, 0x72, 0x83, 0x0a, 0x55, 0x43, 0x16, 0xc0, 0x9d, 0xe9, 0x4a, 0x48, 0x9c, 0x4f, 0x31,
|
||||
0x94, 0x58, 0xf8, 0x03, 0xe5, 0xde, 0xb1, 0x05, 0x3f, 0x5a, 0xe0, 0x4d, 0x44, 0xbe, 0x9a, 0xa5,
|
||||
0x42, 0xce, 0xdf, 0xa8, 0x67, 0x8f, 0xc0, 0x0d, 0x31, 0x8e, 0x73, 0xdf, 0x1e, 0xda, 0xa3, 0xc1,
|
||||
0xe3, 0x07, 0xfb, 0xcd, 0x30, 0x9a, 0x38, 0x87, 0x18, 0xc7, 0x5c, 0xdf, 0x62, 0x9f, 0x81, 0x57,
|
||||
0xe0, 0x3a, 0x8b, 0x45, 0x81, 0xb9, 0xef, 0xa8, 0x9f, 0xb0, 0xed, 0x4f, 0xce, 0x8d, 0x8b, 0x6f,
|
||||
0x2f, 0x05, 0x7f, 0x76, 0xe0, 0xee, 0x4e, 0x28, 0x76, 0x07, 0xac, 0x8d, 0xaa, 0xca, 0xe5, 0xd6,
|
||||
0x86, 0x50, 0xa5, 0x2a, 0x72, 0xb9, 0x55, 0x11, 0xba, 0x56, 0xf3, 0x73, 0xb9, 0x75, 0x4d, 0x68,
|
||||
0xa5, 0xa6, 0xe6, 0x72, 0x6b, 0xc5, 0x3e, 0x84, 0xde, 0x0f, 0x25, 0xca, 0x08, 0x73, 0xdf, 0x55,
|
||||
0x99, 0xff, 0xb7, 0xcd, 0x7c, 0x56, 0xa2, 0xac, 0x78, 0xed, 0xa7, 0x97, 0xaa, 0x89, 0xeb, 0xf1,
|
||||
0xa9, 0x33, 0xd9, 0x0a, 0x62, 0x47, 0x4f, 0xdb, 0xe8, 0x6c, 0x3a, 0xa4, 0x67, 0x46, 0x1d, 0xfa,
|
||||
0x1c, 0x1c, 0xb1, 0xc1, 0xdc, 0xf7, 0x54, 0xfc, 0x77, 0x5f, 0xd3, 0x8c, 0xfd, 0xf1, 0x06, 0xf3,
|
||||
0xaf, 0x92, 0x42, 0x56, 0x5c, 0x5d, 0x67, 0x1f, 0x40, 0x37, 0x4c, 0xe3, 0x54, 0xe6, 0x3e, 0xdc,
|
||||
0x2c, 0xec, 0x90, 0xec, 0xdc, 0xb8, 0x1f, 0x3e, 0x05, 0xaf, 0xf9, 0x2d, 0x51, 0xec, 0x25, 0x56,
|
||||
0xaa, 0x13, 0x1e, 0xa7, 0x23, 0x7b, 0x0f, 0xdc, 0x2b, 0x11, 0x97, 0x7a, 0x42, 0x83, 0xc7, 0x7b,
|
||||
0xdb, 0x30, 0xe3, 0x4d, 0x94, 0x73, 0xed, 0xfc, 0xb2, 0xf3, 0x85, 0x15, 0x2c, 0xc1, 0x55, 0x91,
|
||||
0x5b, 0x33, 0xf6, 0xea, 0x19, 0xab, 0x1d, 0xe8, 0xb4, 0x76, 0xe0, 0x1e, 0xd8, 0x5f, 0xe3, 0xc6,
|
||||
0xac, 0x05, 0x1d, 0x1b, 0x26, 0x38, 0x2d, 0x26, 0xdc, 0x07, 0xf7, 0x52, 0x25, 0xd7, 0xab, 0xa0,
|
||||
0x41, 0xf0, 0x9b, 0x05, 0x0e, 0x25, 0x27, 0xfa, 0xc5, 0xb8, 0x14, 0x61, 0x75, 0x90, 0x96, 0xc9,
|
||||
0x3c, 0xf7, 0xad, 0xa1, 0x3d, 0xb2, 0xf9, 0x8e, 0x8d, 0xbd, 0x05, 0xdd, 0x99, 0xf6, 0x76, 0x86,
|
||||
0xf6, 0xc8, 0xe3, 0x06, 0x51, 0xe8, 0x58, 0xcc, 0x30, 0x36, 0x25, 0x68, 0x40, 0xb7, 0x33, 0x89,
|
||||
0x8b, 0x68, 0x63, 0xca, 0x30, 0x88, 0xec, 0x79, 0xb9, 0x20, 0xbb, 0xae, 0xc4, 0x20, 0x2a, 0x7a,
|
||||
0x26, 0xf2, 0x66, 0xa8, 0x74, 0xa6, 0xc8, 0x79, 0x28, 0xe2, 0x7a, 0xaa, 0x1a, 0x04, 0xbf, 0x5b,
|
||||
0xb4, 0x91, 0x9a, 0x81, 0xaf, 0x74, 0xe8, 0x6d, 0xe8, 0x13, 0x3b, 0x5f, 0x5c, 0x09, 0x69, 0xba,
|
||||
0xd4, 0x23, 0x7c, 0x29, 0x24, 0xfb, 0x14, 0xba, 0xaa, 0xc5, 0xb7, 0x6c, 0x43, 0x1d, 0x4e, 0x75,
|
||||
0x85, 0x9b, 0x6b, 0x0d, 0xa7, 0x9c, 0x16, 0xa7, 0x9a, 0xc7, 0xba, 0xed, 0xc7, 0x3e, 0x02, 0x97,
|
||||
0xc8, 0x59, 0xa9, 0xea, 0x6f, 0x8d, 0xac, 0x29, 0xac, 0x6f, 0x05, 0x17, 0x70, 0x77, 0x27, 0x63,
|
||||
0x93, 0xc9, 0xda, 0xcd, 0xb4, 0xa5, 0x8b, 0x67, 0xe8, 0x41, 0x6a, 0x94, 0x63, 0x8c, 0x61, 0x81,
|
||||
0x73, 0xd5, 0xef, 0x3e, 0x6f, 0x70, 0xf0, 0xb3, 0xb5, 0x8d, 0xab, 0xf2, 0x91, 0xde, 0x84, 0xe9,
|
||||
0x7a, 0x2d, 0x92, 0xb9, 0x09, 0x5d, 0x43, 0xea, 0xdb, 0x7c, 0x66, 0x42, 0x77, 0xe6, 0x33, 0xc2,
|
||||
0x32, 0x33, 0x13, 0xec, 0xc8, 0x8c, 0x0d, 0x61, 0xb0, 0x46, 0x91, 0x97, 0x12, 0xd7, 0x98, 0x14,
|
||||
0xa6, 0x05, 0x6d, 0x13, 0x7b, 0x00, 0xbd, 0x42, 0x2c, 0x5f, 0x10, 0xc9, 0xcd, 0x24, 0x0b, 0xb1,
|
||||
0x3c, 0xc6, 0x8a, 0xbd, 0x03, 0xde, 0x22, 0xc2, 0x78, 0xae, 0x5c, 0x7a, 0x9c, 0x7d, 0x65, 0x38,
|
||||
0xc6, 0x2a, 0xf8, 0xc5, 0x82, 0xee, 0x14, 0xe5, 0x15, 0xca, 0x37, 0x12, 0xb0, 0xb6, 0xc0, 0xdb,
|
||||
0xff, 0x20, 0xf0, 0xce, 0xed, 0x02, 0xef, 0x6e, 0x05, 0xfe, 0x3e, 0xb8, 0x53, 0x19, 0x1e, 0x4d,
|
||||
0x54, 0x45, 0x36, 0xd7, 0x80, 0xd8, 0x38, 0x0e, 0x8b, 0xe8, 0x0a, 0x8d, 0xea, 0x1b, 0x14, 0xfc,
|
||||
0x64, 0x41, 0xf7, 0x44, 0x54, 0x69, 0x59, 0xbc, 0xc2, 0xb0, 0x21, 0x0c, 0xc6, 0x59, 0x16, 0x47,
|
||||
0xa1, 0x28, 0xa2, 0x34, 0x31, 0xd5, 0xb6, 0x4d, 0x74, 0xe3, 0x59, 0xab, 0x77, 0xba, 0xee, 0xb6,
|
||||
0x89, 0xa4, 0xe0, 0x50, 0xe9, 0xb2, 0x16, 0xd9, 0x96, 0x14, 0x68, 0x39, 0x56, 0x4e, 0x7a, 0xe0,
|
||||
0xb8, 0x2c, 0xd2, 0x45, 0x9c, 0x5e, 0xab, 0x97, 0xf4, 0x79, 0x83, 0x83, 0x3f, 0x3a, 0xe0, 0xfc,
|
||||
0x57, 0x7a, 0x7b, 0x07, 0xac, 0xc8, 0x0c, 0xd2, 0x8a, 0x1a, 0xf5, 0xed, 0xb5, 0xd4, 0xd7, 0x87,
|
||||
0x5e, 0x25, 0x45, 0xb2, 0xc4, 0xdc, 0xef, 0x2b, 0xe5, 0xa8, 0xa1, 0xf2, 0xa8, 0x1d, 0xd1, 0xb2,
|
||||
0xeb, 0xf1, 0x1a, 0x36, 0x9c, 0x87, 0x16, 0xe7, 0x3f, 0x31, 0x0a, 0x3d, 0x50, 0x15, 0xf9, 0xbb,
|
||||
0x6d, 0xb9, 0x29, 0xcc, 0xff, 0x9e, 0xde, 0xfe, 0x65, 0x81, 0xdb, 0x2c, 0xcc, 0xe1, 0xee, 0xc2,
|
||||
0x1c, 0x6e, 0x17, 0x66, 0x72, 0x50, 0x2f, 0xcc, 0xe4, 0x80, 0x30, 0x3f, 0xad, 0x17, 0x86, 0x9f,
|
||||
0xd2, 0xb0, 0x9e, 0xca, 0xb4, 0xcc, 0x0e, 0x2a, 0x3d, 0x55, 0x8f, 0x37, 0x98, 0x58, 0xf6, 0xdd,
|
||||
0x0a, 0xa5, 0x69, 0xb5, 0xc7, 0x0d, 0x22, 0x4e, 0x9e, 0x28, 0x31, 0xd1, 0xcd, 0xd5, 0x80, 0xbd,
|
||||
0x0f, 0x2e, 0xa7, 0xe6, 0xa9, 0x0e, 0xef, 0xcc, 0x45, 0x99, 0xb9, 0xf6, 0x52, 0x50, 0xfd, 0xf5,
|
||||
0x64, 0xfe, 0xe1, 0xea, 0x6f, 0xa9, 0x8f, 0xa1, 0x3b, 0x5d, 0x45, 0x8b, 0xa2, 0xfe, 0x9f, 0xfb,
|
||||
0x7f, 0x4b, 0x8c, 0xa2, 0x35, 0x2a, 0x1f, 0x37, 0x57, 0x82, 0x33, 0xf0, 0x1a, 0xe3, 0xb6, 0x1c,
|
||||
0xab, 0x5d, 0x0e, 0x03, 0xe7, 0x22, 0x89, 0x8a, 0x7a, 0x2d, 0xe9, 0x4c, 0x8f, 0x3d, 0x2b, 0x45,
|
||||
0x52, 0x44, 0x45, 0x55, 0xaf, 0x65, 0x8d, 0x83, 0x27, 0xa6, 0x7c, 0x0a, 0x77, 0x91, 0x65, 0x28,
|
||||
0xcd, 0x8a, 0x6b, 0xa0, 0x92, 0xa4, 0xd7, 0xa8, 0xd5, 0xd9, 0xe6, 0x1a, 0x04, 0xdf, 0x83, 0x37,
|
||||
0x8e, 0x51, 0x16, 0xbc, 0x8c, 0xf1, 0xb6, 0x7f, 0xbd, 0x6f, 0xa6, 0xdf, 0x3e, 0xaf, 0x2b, 0xa0,
|
||||
0xf3, 0x76, 0x9d, 0xed, 0x1b, 0xeb, 0x7c, 0x2c, 0x32, 0x71, 0x34, 0x51, 0x3c, 0xb7, 0xb9, 0x41,
|
||||
0xc1, 0x47, 0xe0, 0x90, 0x6c, 0xb4, 0x22, 0x3b, 0xaf, 0x93, 0x9c, 0x59, 0x57, 0x7d, 0xa7, 0x3e,
|
||||
0xf9, 0x3b, 0x00, 0x00, 0xff, 0xff, 0xe5, 0xc0, 0x79, 0x31, 0xb9, 0x0a, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -23,15 +23,24 @@ message Dashboard {
|
|||
}
|
||||
|
||||
message DashboardCell {
|
||||
int32 x = 1; // X-coordinate of Cell in the Dashboard
|
||||
int32 y = 2; // Y-coordinate of Cell in the Dashboard
|
||||
int32 w = 3; // Width of Cell in the Dashboard
|
||||
int32 h = 4; // Height of Cell in the Dashboard
|
||||
repeated Query queries = 5; // Time-series data queries for Dashboard
|
||||
string name = 6; // User-facing name for this Dashboard
|
||||
string type = 7; // Dashboard visualization type
|
||||
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
||||
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
|
||||
int32 x = 1; // X-coordinate of Cell in the Dashboard
|
||||
int32 y = 2; // Y-coordinate of Cell in the Dashboard
|
||||
int32 w = 3; // Width of Cell in the Dashboard
|
||||
int32 h = 4; // Height of Cell in the Dashboard
|
||||
repeated Query queries = 5; // Time-series data queries for Dashboard
|
||||
string name = 6; // User-facing name for this Dashboard
|
||||
string type = 7; // Dashboard visualization type
|
||||
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
||||
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
|
||||
repeated Color colors = 10; // Colors represent encoding data values to color
|
||||
}
|
||||
|
||||
message Color {
|
||||
string ID = 1; // ID is the unique id of the cell color
|
||||
string Type = 2; // Type is how the color is used. Accepted (min,max,threshold)
|
||||
string Hex = 3; // Hex is the hex number of the color
|
||||
string Name = 4; // Name is the user-facing name of the hex color
|
||||
string Value = 5; // Value is the data value mapped to this color
|
||||
}
|
||||
|
||||
message Axis {
|
||||
|
@ -54,18 +63,18 @@ message Template {
|
|||
}
|
||||
|
||||
message TemplateValue {
|
||||
string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
|
||||
string value = 2; // Value is the specific value used to replace a template in an InfluxQL query
|
||||
bool selected = 3; // Selected states that this variable has been picked to use for replacement
|
||||
string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
|
||||
string value = 2; // Value is the specific value used to replace a template in an InfluxQL query
|
||||
bool selected = 3; // Selected states that this variable has been picked to use for replacement
|
||||
}
|
||||
|
||||
message TemplateQuery {
|
||||
string command = 1; // Command is the query itself
|
||||
string db = 2; // DB the database for the query (optional)
|
||||
string rp = 3; // RP is a retention policy and optional;
|
||||
string measurement = 4; // Measurement is the optinally selected measurement for the query
|
||||
string tag_key = 5; // TagKey is the optionally selected tag key for the query
|
||||
string field_key = 6; // FieldKey is the optionally selected field key for the query
|
||||
string command = 1; // Command is the query itself
|
||||
string db = 2; // DB the database for the query (optional)
|
||||
string rp = 3; // RP is a retention policy and optional;
|
||||
string measurement = 4; // Measurement is the optinally selected measurement for the query
|
||||
string tag_key = 5; // TagKey is the optionally selected tag key for the query
|
||||
string field_key = 6; // FieldKey is the optionally selected field key for the query
|
||||
}
|
||||
|
||||
message Server {
|
||||
|
@ -101,31 +110,38 @@ message Cell {
|
|||
}
|
||||
|
||||
message Query {
|
||||
string Command = 1; // Command is the query itself
|
||||
string DB = 2; // DB the database for the query (optional)
|
||||
string RP = 3; // RP is a retention policy and optional;
|
||||
repeated string GroupBys= 4; // GroupBys define the groups to combine in the query
|
||||
repeated string Wheres = 5; // Wheres define the restrictions on the query
|
||||
string Label = 6; // Label is the name of the Y-Axis
|
||||
Range Range = 7; // Range is the upper and lower bound of the Y-Axis
|
||||
string Source = 8; // Source is the optional URI to the data source
|
||||
string Command = 1; // Command is the query itself
|
||||
string DB = 2; // DB the database for the query (optional)
|
||||
string RP = 3; // RP is a retention policy and optional;
|
||||
repeated string GroupBys = 4; // GroupBys define the groups to combine in the query
|
||||
repeated string Wheres = 5; // Wheres define the restrictions on the query
|
||||
string Label = 6; // Label is the name of the Y-Axis
|
||||
Range Range = 7; // Range is the upper and lower bound of the Y-Axis
|
||||
string Source = 8; // Source is the optional URI to the data source
|
||||
repeated TimeShift Shifts = 9; // TimeShift represents a shift to apply to an influxql query's time range
|
||||
}
|
||||
|
||||
message TimeShift {
|
||||
string Label = 1; // Label user facing description
|
||||
string Unit = 2; // Unit influxql time unit representation i.e. ms, s, m, h, d
|
||||
string Quantity = 3; // Quantity number of units
|
||||
}
|
||||
|
||||
message Range {
|
||||
int64 Upper = 1; // Upper is the upper-bound of the range
|
||||
int64 Lower = 2; // Lower is the lower-bound of the range
|
||||
int64 Upper = 1; // Upper is the upper-bound of the range
|
||||
int64 Lower = 2; // Lower is the lower-bound of the range
|
||||
}
|
||||
|
||||
message AlertRule {
|
||||
string ID = 1; // ID is the unique ID of this alert rule
|
||||
string JSON = 2; // JSON byte representation of the alert
|
||||
int64 SrcID = 3; // SrcID is the id of the source this alert is associated with
|
||||
int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with
|
||||
string ID = 1; // ID is the unique ID of this alert rule
|
||||
string JSON = 2; // JSON byte representation of the alert
|
||||
int64 SrcID = 3; // SrcID is the id of the source this alert is associated with
|
||||
int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with
|
||||
}
|
||||
|
||||
message User {
|
||||
uint64 ID = 1; // ID is the unique ID of this user
|
||||
string Name = 2; // Name is the user's login name
|
||||
uint64 ID = 1; // ID is the unique ID of this user
|
||||
string Name = 2; // Name is the user's login name
|
||||
}
|
||||
|
||||
// The following is a vim modeline, it autoconfigures vim to have the
|
||||
|
|
|
@ -163,6 +163,7 @@ func Test_MarshalDashboard(t *testing.T) {
|
|||
Upper: int64(100),
|
||||
},
|
||||
Source: "/chronograf/v1/sources/1",
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -176,6 +177,22 @@ func Test_MarshalDashboard(t *testing.T) {
|
|||
},
|
||||
},
|
||||
Type: "line",
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "myid",
|
||||
Type: "min",
|
||||
Hex: "#234567",
|
||||
Name: "Laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "id2",
|
||||
Type: "max",
|
||||
Hex: "#876543",
|
||||
Name: "Solitude",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
|
@ -210,6 +227,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -217,6 +235,22 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
LegacyBounds: [2]int64{0, 5},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "myid",
|
||||
Type: "min",
|
||||
Hex: "#234567",
|
||||
Name: "Laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "id2",
|
||||
Type: "max",
|
||||
Hex: "#876543",
|
||||
Name: "Solitude",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
@ -241,6 +275,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -250,6 +285,22 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
Scale: "linear",
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "myid",
|
||||
Type: "min",
|
||||
Hex: "#234567",
|
||||
Name: "Laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "id2",
|
||||
Type: "max",
|
||||
Hex: "#876543",
|
||||
Name: "Solitude",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
@ -285,6 +336,7 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
|
|||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -292,6 +344,22 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
|
|||
LegacyBounds: [2]int64{},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "myid",
|
||||
Type: "min",
|
||||
Hex: "#234567",
|
||||
Name: "Laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "id2",
|
||||
Type: "max",
|
||||
Hex: "#876543",
|
||||
Name: "Solitude",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
@ -316,6 +384,7 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
|
|||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -325,6 +394,22 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
|
|||
Scale: "linear",
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "myid",
|
||||
Type: "min",
|
||||
Hex: "#234567",
|
||||
Name: "Laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "id2",
|
||||
Type: "max",
|
||||
Hex: "#876543",
|
||||
Name: "Solitude",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"name": "MySQL – Reads/Second",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"commands_select\")) AS selects_per_second FROM mysql",
|
||||
"query": "SELECT non_negative_derivative(last(\"commands_select\"), 1s) AS selects_per_second FROM mysql",
|
||||
"groupbys": [
|
||||
"\"server\""
|
||||
],
|
||||
|
@ -30,7 +30,7 @@
|
|||
"name": "MySQL – Writes/Second",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"commands_insert\")) AS inserts_per_second, non_negative_derivative(max(\"commands_update\")) AS updates_per_second, non_negative_derivative(max(\"commands_delete\")) AS deletes_per_second FROM mysql",
|
||||
"query": "SELECT non_negative_derivative(last(\"commands_insert\"), 1s) AS inserts_per_second, non_negative_derivative(last(\"commands_update\"), 1s) AS updates_per_second, non_negative_derivative(last(\"commands_delete\"), 1s) AS deletes_per_second FROM mysql",
|
||||
"groupbys": [
|
||||
"\"server\""
|
||||
],
|
||||
|
@ -47,7 +47,7 @@
|
|||
"name": "MySQL – Connections/Second",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"threads_connected\")) AS cxn_per_second, non_negative_derivative(max(\"threads_running\")) AS threads_running_per_second FROM mysql",
|
||||
"query": "SELECT non_negative_derivative(last(\"threads_connected\"), 1s) AS cxn_per_second, non_negative_derivative(last(\"threads_running\"), 1s) AS threads_running_per_second FROM mysql",
|
||||
"groupbys": [
|
||||
"\"server\""
|
||||
],
|
||||
|
@ -64,7 +64,7 @@
|
|||
"name": "MySQL – Connections Errors/Second",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT non_negative_derivative(max(\"connection_errors_max_connections\")) AS cxn_errors_per_second, non_negative_derivative(max(\"connection_errors_internal\")) AS internal_cxn_errors_per_second, non_negative_derivative(max(\"aborted_connects\")) AS cxn_aborted_per_second FROM mysql",
|
||||
"query": "SELECT non_negative_derivative(last(\"connection_errors_max_connections\"), 1s) AS cxn_errors_per_second, non_negative_derivative(last(\"connection_errors_internal\"), 1s) AS internal_cxn_errors_per_second, non_negative_derivative(last(\"aborted_connects\"), 1s) AS cxn_aborted_per_second FROM mysql",
|
||||
"groupbys": [
|
||||
"\"server\""
|
||||
],
|
||||
|
|
|
@ -20,6 +20,8 @@ const (
|
|||
ErrAuthentication = Error("user not authenticated")
|
||||
ErrUninitialized = Error("client uninitialized. Call Open() method")
|
||||
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
|
||||
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'")
|
||||
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
||||
)
|
||||
|
||||
// Error is a domain error encountered while processing chronograf requests
|
||||
|
@ -171,6 +173,7 @@ type DashboardQuery struct {
|
|||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
||||
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
|
||||
Source string `json:"source"` // Source is the optional URI to the data source for this queryConfig
|
||||
Shifts []TimeShift `json:"-"` // Shifts represents shifts to apply to an influxql query's time range. Clients expect the shift to be in the generated QueryConfig
|
||||
}
|
||||
|
||||
// TemplateQuery is used to retrieve choices for template replacement
|
||||
|
@ -284,6 +287,13 @@ type DurationRange struct {
|
|||
Lower string `json:"lower"`
|
||||
}
|
||||
|
||||
// TimeShift represents a shift to apply to an influxql query's time range
|
||||
type TimeShift struct {
|
||||
Label string `json:"label"` // Label user facing description
|
||||
Unit string `json:"unit"` // Unit influxql time unit representation i.e. ms, s, m, h, d
|
||||
Quantity string `json:"quantity"` // Quantity number of units
|
||||
}
|
||||
|
||||
// QueryConfig represents UI query from the data explorer
|
||||
type QueryConfig struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
|
@ -297,6 +307,7 @@ type QueryConfig struct {
|
|||
Fill string `json:"fill,omitempty"`
|
||||
RawText *string `json:"rawText"`
|
||||
Range *DurationRange `json:"range"`
|
||||
Shifts []TimeShift `json:"shifts"`
|
||||
}
|
||||
|
||||
// KapacitorNode adds arguments and properties to an alert
|
||||
|
@ -443,17 +454,27 @@ type Axis struct {
|
|||
Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear"
|
||||
}
|
||||
|
||||
// CellColor represents the encoding of data into visualizations
|
||||
type CellColor struct {
|
||||
ID string `json:"id"` // ID is the unique id of the cell color
|
||||
Type string `json:"type"` // Type is how the color is used. Accepted (min,max,threshold)
|
||||
Hex string `json:"hex"` // Hex is the hex number of the color
|
||||
Name string `json:"name"` // Name is the user-facing name of the hex color
|
||||
Value string `json:"value"` // Value is the data value mapped to this color
|
||||
}
|
||||
|
||||
// DashboardCell holds visual and query information for a cell
|
||||
type DashboardCell struct {
|
||||
ID string `json:"i"`
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
W int32 `json:"w"`
|
||||
H int32 `json:"h"`
|
||||
Name string `json:"name"`
|
||||
Queries []DashboardQuery `json:"queries"`
|
||||
Axes map[string]Axis `json:"axes"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"i"`
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
W int32 `json:"w"`
|
||||
H int32 `json:"h"`
|
||||
Name string `json:"name"`
|
||||
Queries []DashboardQuery `json:"queries"`
|
||||
Axes map[string]Axis `json:"axes"`
|
||||
Type string `json:"type"`
|
||||
CellColors []CellColor `json:"colors"`
|
||||
}
|
||||
|
||||
// DashboardsStore is the storage and retrieval of dashboards
|
||||
|
|
|
@ -51,13 +51,13 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewClientWithTimeSeries initializes a Client with a known set of TimeSeries.
|
||||
func NewClientWithTimeSeries(lg chronograf.Logger, mu, username, password string, tls bool, series ...chronograf.TimeSeries) (*Client, error) {
|
||||
func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.Authorizer, tls bool, series ...chronograf.TimeSeries) (*Client, error) {
|
||||
metaURL, err := parseMetaURL(mu, tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaURL.User = url.UserPassword(username, password)
|
||||
ctrl := NewMetaClient(metaURL)
|
||||
|
||||
ctrl := NewMetaClient(metaURL, authorizer)
|
||||
c := &Client{
|
||||
Ctrl: ctrl,
|
||||
UsersStore: &UserStore{
|
||||
|
@ -83,15 +83,15 @@ func NewClientWithTimeSeries(lg chronograf.Logger, mu, username, password string
|
|||
// NewClientWithURL initializes an Enterprise client with a URL to a Meta Node.
|
||||
// Acceptable URLs include host:port combinations as well as scheme://host:port
|
||||
// varieties. TLS is used when the URL contains "https" or when the TLS
|
||||
// parameter is set. The latter option is provided for host:port combinations
|
||||
// Username and Password are used for Basic Auth
|
||||
func NewClientWithURL(mu, username, password string, tls bool, lg chronograf.Logger) (*Client, error) {
|
||||
// parameter is set. authorizer will add the correct `Authorization` headers
|
||||
// on the out-bound request.
|
||||
func NewClientWithURL(mu string, authorizer influx.Authorizer, tls bool, lg chronograf.Logger) (*Client, error) {
|
||||
metaURL, err := parseMetaURL(mu, tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metaURL.User = url.UserPassword(username, password)
|
||||
ctrl := NewMetaClient(metaURL)
|
||||
|
||||
ctrl := NewMetaClient(metaURL, authorizer)
|
||||
return &Client{
|
||||
Ctrl: ctrl,
|
||||
UsersStore: &UserStore{
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/enterprise"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
)
|
||||
|
||||
|
@ -75,7 +76,16 @@ func Test_Enterprise_IssuesQueries(t *testing.T) {
|
|||
func Test_Enterprise_AdvancesDataNodes(t *testing.T) {
|
||||
m1 := NewMockTimeSeries("http://host-1.example.com:8086")
|
||||
m2 := NewMockTimeSeries("http://host-2.example.com:8086")
|
||||
cl, err := enterprise.NewClientWithTimeSeries(log.New(log.DebugLevel), "http://meta.example.com:8091", "marty", "thelake", false, chronograf.TimeSeries(m1), chronograf.TimeSeries(m2))
|
||||
cl, err := enterprise.NewClientWithTimeSeries(
|
||||
log.New(log.DebugLevel),
|
||||
"http://meta.example.com:8091",
|
||||
&influx.BasicAuth{
|
||||
Username: "marty",
|
||||
Password: "thelake",
|
||||
},
|
||||
false,
|
||||
chronograf.TimeSeries(m1),
|
||||
chronograf.TimeSeries(m2))
|
||||
if err != nil {
|
||||
t.Error("Unexpected error while initializing client: err:", err)
|
||||
}
|
||||
|
@ -124,7 +134,14 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, testURL := range urls {
|
||||
_, err := enterprise.NewClientWithURL(testURL.url, testURL.username, testURL.password, testURL.tls, log.New(log.DebugLevel))
|
||||
_, err := enterprise.NewClientWithURL(
|
||||
testURL.url,
|
||||
&influx.BasicAuth{
|
||||
Username: testURL.username,
|
||||
Password: testURL.password,
|
||||
},
|
||||
testURL.tls,
|
||||
log.New(log.DebugLevel))
|
||||
if err != nil && !testURL.shouldErr {
|
||||
t.Errorf("Unexpected error creating Client with URL %s and TLS preference %t. err: %s", testURL.url, testURL.tls, err.Error())
|
||||
} else if err == nil && testURL.shouldErr {
|
||||
|
@ -135,7 +152,14 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
|
|||
|
||||
func Test_Enterprise_ComplainsIfNotOpened(t *testing.T) {
|
||||
m1 := NewMockTimeSeries("http://host-1.example.com:8086")
|
||||
cl, err := enterprise.NewClientWithTimeSeries(log.New(log.DebugLevel), "http://meta.example.com:8091", "docbrown", "1.21 gigawatts", false, chronograf.TimeSeries(m1))
|
||||
cl, err := enterprise.NewClientWithTimeSeries(
|
||||
log.New(log.DebugLevel),
|
||||
"http://meta.example.com:8091",
|
||||
&influx.BasicAuth{
|
||||
Username: "docbrown",
|
||||
Password: "1.21 gigawatts",
|
||||
},
|
||||
false, chronograf.TimeSeries(m1))
|
||||
if err != nil {
|
||||
t.Error("Expected ErrUnitialized, but was this err:", err)
|
||||
}
|
||||
|
|
|
@ -11,31 +11,32 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
||||
type client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
|
||||
// MetaClient represents a Meta node in an Influx Enterprise cluster
|
||||
type MetaClient struct {
|
||||
URL *url.URL
|
||||
client client
|
||||
URL *url.URL
|
||||
client client
|
||||
authorizer influx.Authorizer
|
||||
}
|
||||
|
||||
type ClientBuilder func() client
|
||||
|
||||
// NewMetaClient represents a meta node in an Influx Enterprise cluster
|
||||
func NewMetaClient(url *url.URL) *MetaClient {
|
||||
func NewMetaClient(url *url.URL, authorizer influx.Authorizer) *MetaClient {
|
||||
return &MetaClient{
|
||||
URL: url,
|
||||
client: &defaultClient{},
|
||||
URL: url,
|
||||
client: &defaultClient{},
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCluster returns the cluster configuration (not health)
|
||||
func (m *MetaClient) ShowCluster(ctx context.Context) (*Cluster, error) {
|
||||
res, err := m.Do(ctx, "GET", "/show-cluster", nil, nil)
|
||||
res, err := m.Do(ctx, "/show-cluster", "GET", m.authorizer, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ func (m *MetaClient) Users(ctx context.Context, name *string) (*Users, error) {
|
|||
if name != nil {
|
||||
params["name"] = *name
|
||||
}
|
||||
res, err := m.Do(ctx, "GET", "/user", params, nil)
|
||||
res, err := m.Do(ctx, "/user", "GET", m.authorizer, params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -118,39 +119,10 @@ func (m *MetaClient) DeleteUser(ctx context.Context, name string) error {
|
|||
return m.Post(ctx, "/user", a, nil)
|
||||
}
|
||||
|
||||
// RemoveAllUserPerms revokes all permissions for a user in Influx Enterprise
|
||||
func (m *MetaClient) RemoveAllUserPerms(ctx context.Context, name string) error {
|
||||
user, err := m.User(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No permissions to remove
|
||||
if len(user.Permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserPerms revokes permissions for a user in Influx Enterprise
|
||||
func (m *MetaClient) RemoveUserPerms(ctx context.Context, name string, perms Permissions) error {
|
||||
a := &UserAction{
|
||||
Action: "remove-permissions",
|
||||
User: user,
|
||||
}
|
||||
return m.Post(ctx, "/user", a, nil)
|
||||
}
|
||||
|
||||
// SetUserPerms removes all permissions and then adds the requested perms
|
||||
func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
|
||||
err := m.RemoveAllUserPerms(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No permissions to add, so, user is in the right state
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
a := &UserAction{
|
||||
Action: "add-permissions",
|
||||
User: &User{
|
||||
Name: name,
|
||||
Permissions: perms,
|
||||
|
@ -159,6 +131,38 @@ func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permis
|
|||
return m.Post(ctx, "/user", a, nil)
|
||||
}
|
||||
|
||||
// SetUserPerms removes permissions not in set and then adds the requested perms
|
||||
func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
|
||||
user, err := m.User(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
revoke, add := permissionsDifference(perms, user.Permissions)
|
||||
|
||||
// first, revoke all the permissions the user currently has, but,
|
||||
// shouldn't...
|
||||
if len(revoke) > 0 {
|
||||
err := m.RemoveUserPerms(ctx, name, revoke)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ... next, add any permissions the user should have
|
||||
if len(add) > 0 {
|
||||
a := &UserAction{
|
||||
Action: "add-permissions",
|
||||
User: &User{
|
||||
Name: name,
|
||||
Permissions: add,
|
||||
},
|
||||
}
|
||||
return m.Post(ctx, "/user", a, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserRoles returns a map of users to all of their current roles
|
||||
func (m *MetaClient) UserRoles(ctx context.Context) (map[string]Roles, error) {
|
||||
res, err := m.Roles(ctx, nil)
|
||||
|
@ -186,7 +190,7 @@ func (m *MetaClient) Roles(ctx context.Context, name *string) (*Roles, error) {
|
|||
if name != nil {
|
||||
params["name"] = *name
|
||||
}
|
||||
res, err := m.Do(ctx, "GET", "/role", params, nil)
|
||||
res, err := m.Do(ctx, "/role", "GET", m.authorizer, params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -235,39 +239,10 @@ func (m *MetaClient) DeleteRole(ctx context.Context, name string) error {
|
|||
return m.Post(ctx, "/role", a, nil)
|
||||
}
|
||||
|
||||
// RemoveAllRolePerms removes all permissions from a role
|
||||
func (m *MetaClient) RemoveAllRolePerms(ctx context.Context, name string) error {
|
||||
role, err := m.Role(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No permissions to remove
|
||||
if len(role.Permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRolePerms revokes permissions from a role
|
||||
func (m *MetaClient) RemoveRolePerms(ctx context.Context, name string, perms Permissions) error {
|
||||
a := &RoleAction{
|
||||
Action: "remove-permissions",
|
||||
Role: role,
|
||||
}
|
||||
return m.Post(ctx, "/role", a, nil)
|
||||
}
|
||||
|
||||
// SetRolePerms removes all permissions and then adds the requested perms to role
|
||||
func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
|
||||
err := m.RemoveAllRolePerms(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No permissions to add, so, role is in the right state
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
a := &RoleAction{
|
||||
Action: "add-permissions",
|
||||
Role: &Role{
|
||||
Name: name,
|
||||
Permissions: perms,
|
||||
|
@ -276,7 +251,39 @@ func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permis
|
|||
return m.Post(ctx, "/role", a, nil)
|
||||
}
|
||||
|
||||
// SetRoleUsers removes all users and then adds the requested users to role
|
||||
// SetRolePerms removes permissions not in set and then adds the requested perms to role
|
||||
func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
|
||||
role, err := m.Role(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
revoke, add := permissionsDifference(perms, role.Permissions)
|
||||
|
||||
// first, revoke all the permissions the role currently has, but,
|
||||
// shouldn't...
|
||||
if len(revoke) > 0 {
|
||||
err := m.RemoveRolePerms(ctx, name, revoke)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ... next, add any permissions the role should have
|
||||
if len(add) > 0 {
|
||||
a := &RoleAction{
|
||||
Action: "add-permissions",
|
||||
Role: &Role{
|
||||
Name: name,
|
||||
Permissions: add,
|
||||
},
|
||||
}
|
||||
return m.Post(ctx, "/role", a, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRoleUsers removes users not in role and then adds the requested users to role
|
||||
func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
|
||||
role, err := m.Role(ctx, name)
|
||||
if err != nil {
|
||||
|
@ -320,6 +327,29 @@ func Difference(wants []string, haves []string) (revoke []string, add []string)
|
|||
return
|
||||
}
|
||||
|
||||
func permissionsDifference(wants Permissions, haves Permissions) (revoke Permissions, add Permissions) {
|
||||
revoke = make(Permissions)
|
||||
add = make(Permissions)
|
||||
for scope, want := range wants {
|
||||
have, ok := haves[scope]
|
||||
if ok {
|
||||
r, a := Difference(want, have)
|
||||
revoke[scope] = r
|
||||
add[scope] = a
|
||||
} else {
|
||||
add[scope] = want
|
||||
}
|
||||
}
|
||||
|
||||
for scope, have := range haves {
|
||||
_, ok := wants[scope]
|
||||
if !ok {
|
||||
revoke[scope] = have
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddRoleUsers updates a role to have additional users.
|
||||
func (m *MetaClient) AddRoleUsers(ctx context.Context, name string, users []string) error {
|
||||
// No permissions to add, so, role is in the right state
|
||||
|
@ -361,7 +391,7 @@ func (m *MetaClient) Post(ctx context.Context, path string, action interface{},
|
|||
return err
|
||||
}
|
||||
body := bytes.NewReader(b)
|
||||
_, err = m.Do(ctx, "POST", path, params, body)
|
||||
_, err = m.Do(ctx, path, "POST", m.authorizer, params, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -373,7 +403,7 @@ type defaultClient struct {
|
|||
}
|
||||
|
||||
// Do is a helper function to interface with Influx Enterprise's Meta API
|
||||
func (d *defaultClient) Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
func (d *defaultClient) Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
p := url.Values{}
|
||||
for k, v := range params {
|
||||
p.Add(k, v)
|
||||
|
@ -391,15 +421,23 @@ func (d *defaultClient) Do(URL *url.URL, path, method string, params map[string]
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
if authorizer != nil {
|
||||
if err = authorizer.Set(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Meta servers will redirect (307) to leader. We need
|
||||
// special handling to preserve authentication headers.
|
||||
client := &http.Client{
|
||||
CheckRedirect: d.AuthedCheckRedirect,
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -437,14 +475,14 @@ func (d *defaultClient) AuthedCheckRedirect(req *http.Request, via []*http.Reque
|
|||
}
|
||||
|
||||
// Do is a cancelable function to interface with Influx Enterprise's Meta API
|
||||
func (m *MetaClient) Do(ctx context.Context, method, path string, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
func (m *MetaClient) Do(ctx context.Context, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
type result struct {
|
||||
Response *http.Response
|
||||
Err error
|
||||
}
|
||||
resps := make(chan (result))
|
||||
go func() {
|
||||
resp, err := m.client.Do(m.URL, path, method, params, body)
|
||||
resp, err := m.client.Do(m.URL, path, method, authorizer, params, body)
|
||||
resps <- result{resp, err}
|
||||
}()
|
||||
|
||||
|
|
|
@ -11,13 +11,16 @@ import (
|
|||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
||||
func TestMetaClient_ShowCluster(t *testing.T) {
|
||||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
tests := []struct {
|
||||
|
@ -128,7 +131,7 @@ func TestMetaClient_Users(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -265,7 +268,7 @@ func TestMetaClient_User(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -366,7 +369,7 @@ func TestMetaClient_CreateUser(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -437,7 +440,7 @@ func TestMetaClient_ChangePassword(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -509,7 +512,7 @@ func TestMetaClient_DeleteUser(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -578,7 +581,7 @@ func TestMetaClient_SetUserPerms(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -595,7 +598,7 @@ func TestMetaClient_SetUserPerms(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful set permissions User",
|
||||
name: "Remove all permissions for a user",
|
||||
fields: fields{
|
||||
URL: &url.URL{
|
||||
Host: "twinpinesmall.net:8091",
|
||||
|
@ -615,7 +618,7 @@ func TestMetaClient_SetUserPerms(t *testing.T) {
|
|||
wantRm: `{"action":"remove-permissions","user":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
|
||||
},
|
||||
{
|
||||
name: "Successful set permissions User",
|
||||
name: "Remove some permissions and add others",
|
||||
fields: fields{
|
||||
URL: &url.URL{
|
||||
Host: "twinpinesmall.net:8091",
|
||||
|
@ -699,7 +702,7 @@ func TestMetaClient_Roles(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -798,7 +801,7 @@ func TestMetaClient_Role(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -881,7 +884,7 @@ func TestMetaClient_UserRoles(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -985,7 +988,7 @@ func TestMetaClient_CreateRole(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -1051,7 +1054,7 @@ func TestMetaClient_DeleteRole(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -1120,7 +1123,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -1137,7 +1140,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful set permissions role",
|
||||
name: "Remove all roles from user",
|
||||
fields: fields{
|
||||
URL: &url.URL{
|
||||
Host: "twinpinesmall.net:8091",
|
||||
|
@ -1154,10 +1157,10 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
ctx: context.Background(),
|
||||
name: "admin",
|
||||
},
|
||||
wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
|
||||
wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
|
||||
},
|
||||
{
|
||||
name: "Successful set single permissions role",
|
||||
name: "Remove some users and add permissions to other",
|
||||
fields: fields{
|
||||
URL: &url.URL{
|
||||
Host: "twinpinesmall.net:8091",
|
||||
|
@ -1179,7 +1182,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
|
||||
wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
|
||||
wantAdd: `{"action":"add-permissions","role":{"name":"admin","permissions":{"telegraf":["ReadData"]}}}`,
|
||||
},
|
||||
}
|
||||
|
@ -1218,7 +1221,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
|
||||
got, _ := ioutil.ReadAll(prm.Body)
|
||||
if string(got) != tt.wantRm {
|
||||
t.Errorf("%q. MetaClient.SetRolePerms() = %v, want %v", tt.name, string(got), tt.wantRm)
|
||||
t.Errorf("%q. MetaClient.SetRolePerms() removal = \n%v\n, want \n%v\n", tt.name, string(got), tt.wantRm)
|
||||
}
|
||||
if tt.wantAdd != "" {
|
||||
prm := reqs[2]
|
||||
|
@ -1231,7 +1234,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
|
|||
|
||||
got, _ := ioutil.ReadAll(prm.Body)
|
||||
if string(got) != tt.wantAdd {
|
||||
t.Errorf("%q. MetaClient.SetRolePerms() = %v, want %v", tt.name, string(got), tt.wantAdd)
|
||||
t.Errorf("%q. MetaClient.SetRolePerms() addition = \n%v\n, want \n%v\n", tt.name, string(got), tt.wantAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1241,7 +1244,7 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
|
|||
type fields struct {
|
||||
URL *url.URL
|
||||
client interface {
|
||||
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
|
@ -1361,7 +1364,7 @@ func NewMockClient(code int, body []byte, headers http.Header, err error) *MockC
|
|||
}
|
||||
}
|
||||
|
||||
func (c *MockClient) Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
func (c *MockClient) Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("NIL MockClient")
|
||||
}
|
||||
|
@ -1453,3 +1456,71 @@ func Test_AuthedCheckRedirect_Do(t *testing.T) {
|
|||
t.Errorf("result = %q; want ok", got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_defaultClient_Do(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
method string
|
||||
authorizer influx.Authorizer
|
||||
params map[string]string
|
||||
body io.Reader
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test authorizer",
|
||||
args: args{
|
||||
path: "/tictactoe",
|
||||
method: "GET",
|
||||
authorizer: &influx.BasicAuth{
|
||||
Username: "Steven Falken",
|
||||
Password: "JOSHUA",
|
||||
},
|
||||
},
|
||||
want: "Basic U3RldmVuIEZhbGtlbjpKT1NIVUE=",
|
||||
},
|
||||
{
|
||||
name: "test authorizer",
|
||||
args: args{
|
||||
path: "/tictactoe",
|
||||
method: "GET",
|
||||
authorizer: &influx.BearerJWT{
|
||||
Username: "minifig",
|
||||
SharedSecret: "legos",
|
||||
Now: func() time.Time { return time.Time{} },
|
||||
},
|
||||
},
|
||||
want: "Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOi02MjEzNTU5Njc0MCwidXNlcm5hbWUiOiJtaW5pZmlnIn0.uwFGBQ3MykqEmk9Zx0sBdJGefcESVEXG_qt0C1J8b_aS62EAES-Q1FwtURsbITNvSnfzMxYFnkbSG0AA1pEzWw",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tictactoe" {
|
||||
t.Fatal("Expected request to '/query' but was", r.URL.Path)
|
||||
}
|
||||
got, ok := r.Header["Authorization"]
|
||||
if !ok {
|
||||
t.Fatal("No Authorization header")
|
||||
}
|
||||
if got[0] != tt.want {
|
||||
t.Fatalf("Expected auth %s got %s", tt.want, got)
|
||||
}
|
||||
rw.Write([]byte(`{}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
d := &defaultClient{}
|
||||
u, _ := url.Parse(ts.URL)
|
||||
_, err := d.Do(u, tt.args.path, tt.args.method, tt.args.authorizer, tt.args.params, tt.args.body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("defaultClient.Do() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,44 +70,49 @@ func (c *UserStore) Update(ctx context.Context, u *chronograf.User) error {
|
|||
return c.Ctrl.ChangePassword(ctx, u.Name, u.Passwd)
|
||||
}
|
||||
|
||||
// Make a list of the roles we want this user to have:
|
||||
want := make([]string, len(u.Roles))
|
||||
for i, r := range u.Roles {
|
||||
want[i] = r.Name
|
||||
}
|
||||
if u.Roles != nil {
|
||||
// Make a list of the roles we want this user to have:
|
||||
want := make([]string, len(u.Roles))
|
||||
for i, r := range u.Roles {
|
||||
want[i] = r.Name
|
||||
}
|
||||
|
||||
// Find the list of all roles this user is currently in
|
||||
userRoles, err := c.UserRoles(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Make a list of the roles the user currently has
|
||||
roles := userRoles[u.Name]
|
||||
have := make([]string, len(roles.Roles))
|
||||
for i, r := range roles.Roles {
|
||||
have[i] = r.Name
|
||||
}
|
||||
// Find the list of all roles this user is currently in
|
||||
userRoles, err := c.UserRoles(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Make a list of the roles the user currently has
|
||||
roles := userRoles[u.Name]
|
||||
have := make([]string, len(roles.Roles))
|
||||
for i, r := range roles.Roles {
|
||||
have[i] = r.Name
|
||||
}
|
||||
|
||||
// Calculate the roles the user will be removed from and the roles the user
|
||||
// will be added to.
|
||||
revoke, add := Difference(want, have)
|
||||
// Calculate the roles the user will be removed from and the roles the user
|
||||
// will be added to.
|
||||
revoke, add := Difference(want, have)
|
||||
|
||||
// First, add the user to the new roles
|
||||
for _, role := range add {
|
||||
if err := c.Ctrl.AddRoleUsers(ctx, role, []string{u.Name}); err != nil {
|
||||
return err
|
||||
// First, add the user to the new roles
|
||||
for _, role := range add {
|
||||
if err := c.Ctrl.AddRoleUsers(ctx, role, []string{u.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ... and now remove the user from an extra roles
|
||||
for _, role := range revoke {
|
||||
if err := c.Ctrl.RemoveRoleUsers(ctx, role, []string{u.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... and now remove the user from an extra roles
|
||||
for _, role := range revoke {
|
||||
if err := c.Ctrl.RemoveRoleUsers(ctx, role, []string{u.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Permissions != nil {
|
||||
perms := ToEnterprise(u.Permissions)
|
||||
return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
|
||||
}
|
||||
|
||||
perms := ToEnterprise(u.Permissions)
|
||||
return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
|
||||
return nil
|
||||
}
|
||||
|
||||
// All is all users in influx
|
||||
|
|
|
@ -54,6 +54,7 @@ func (b *BasicAuth) Set(r *http.Request) error {
|
|||
type BearerJWT struct {
|
||||
Username string
|
||||
SharedSecret string
|
||||
Now Now
|
||||
}
|
||||
|
||||
// Set adds an Authorization Bearer to the request if has a shared secret
|
||||
|
@ -70,7 +71,10 @@ func (b *BearerJWT) Set(r *http.Request) error {
|
|||
|
||||
// Token returns the expected InfluxDB JWT signed with the sharedSecret
|
||||
func (b *BearerJWT) Token(username string) (string, error) {
|
||||
return JWT(username, b.SharedSecret, time.Now)
|
||||
if b.Now == nil {
|
||||
b.Now = time.Now
|
||||
}
|
||||
return JWT(username, b.SharedSecret, b.Now)
|
||||
}
|
||||
|
||||
// Now returns the current time
|
||||
|
|
|
@ -214,6 +214,9 @@ var trigger = data
|
|||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -300,6 +303,9 @@ var trigger = data
|
|||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -542,6 +548,9 @@ var trigger = data
|
|||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -625,6 +634,9 @@ var trigger = data
|
|||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -1380,6 +1392,9 @@ trigger
|
|||
|eval(lambda: "emitted")
|
||||
.as('value')
|
||||
.keep('value', messageField, durationField)
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
|
|
@ -20,11 +20,14 @@ func InfluxOut(rule chronograf.AlertRule) (string, error) {
|
|||
return fmt.Sprintf(`
|
||||
trigger
|
||||
%s
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
`, rename), nil
|
||||
|
|
|
@ -14,6 +14,9 @@ func TestInfluxOut(t *testing.T) {
|
|||
|eval(lambda: "emitted")
|
||||
.as('value')
|
||||
.keep('value', messageField, durationField)
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
|
|
@ -189,6 +189,9 @@ var trigger = data
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -333,6 +336,9 @@ var trigger = data
|
|||
.email()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -479,6 +485,9 @@ var trigger = data
|
|||
.email()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -636,6 +645,9 @@ var trigger = data
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -792,6 +804,9 @@ var trigger = data
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -948,6 +963,9 @@ var trigger = data
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -1087,6 +1105,9 @@ var trigger = data
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -1254,6 +1275,9 @@ var trigger = past
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -1421,6 +1445,9 @@ var trigger = past
|
|||
.slack()
|
||||
|
||||
trigger
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
@ -1567,6 +1594,9 @@ trigger
|
|||
|eval(lambda: "emitted")
|
||||
.as('value')
|
||||
.keep('value', messageField, durationField)
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
|
|
|
@ -76,12 +76,12 @@ func Vars(rule chronograf.AlertRule) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// NotEmpty is an error collector testing strings for existence.
|
||||
// NotEmpty is an error collector checking if strings are empty values
|
||||
type NotEmpty struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Valid checks if string is not empty
|
||||
// Valid checks if string s is empty and if so reports an error using name
|
||||
func (n *NotEmpty) Valid(name, s string) error {
|
||||
if n.Err != nil {
|
||||
return n.Err
|
||||
|
@ -93,7 +93,7 @@ func (n *NotEmpty) Valid(name, s string) error {
|
|||
return n.Err
|
||||
}
|
||||
|
||||
// Escape escapes all single quoted strings
|
||||
// Escape sanitizes strings with single quotes for kapacitor
|
||||
func Escape(str string) string {
|
||||
return strings.Replace(str, "'", `\'`, -1)
|
||||
}
|
||||
|
@ -254,5 +254,10 @@ func formatValue(value string) string {
|
|||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return value
|
||||
}
|
||||
return "'" + value + "'"
|
||||
|
||||
// If the value is a kapacitor boolean value perform no formatting
|
||||
if value == "TRUE" || value == "FALSE" {
|
||||
return value
|
||||
}
|
||||
return "'" + Escape(value) + "'"
|
||||
}
|
||||
|
|
|
@ -49,3 +49,39 @@ func TestVarsCritStringEqual(t *testing.T) {
|
|||
t.Errorf("Error validating alert: %v %s", err, tick)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "parses floats",
|
||||
value: "3.14",
|
||||
want: "3.14",
|
||||
},
|
||||
{
|
||||
name: "parses booleans",
|
||||
value: "TRUE",
|
||||
want: "TRUE",
|
||||
},
|
||||
{
|
||||
name: "single quotes for strings",
|
||||
value: "up",
|
||||
want: "'up'",
|
||||
},
|
||||
{
|
||||
name: "handles escaping of single quotes",
|
||||
value: "down's",
|
||||
want: "'down\\'s'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := formatValue(tt.value); got != tt.want {
|
||||
t.Errorf("formatValue() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ type Generic struct {
|
|||
AuthURL string
|
||||
TokenURL string
|
||||
APIURL string // APIURL returns OpenID Userinfo
|
||||
APIKey string // APIKey is the JSON key to lookup email address in APIURL response
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
|
@ -69,9 +70,7 @@ func (g *Generic) Config() *oauth2.Config {
|
|||
|
||||
// PrincipalID returns the email address of the user.
|
||||
func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
|
||||
res := struct {
|
||||
Email string `json:"email"`
|
||||
}{}
|
||||
res := map[string]interface{}{}
|
||||
|
||||
r, err := provider.Get(g.APIURL)
|
||||
if err != nil {
|
||||
|
@ -83,7 +82,11 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
email := res.Email
|
||||
email := ""
|
||||
value := res[g.APIKey]
|
||||
if e, ok := value.(string); ok {
|
||||
email = e
|
||||
}
|
||||
|
||||
// If we did not receive an email address, try to lookup the email
|
||||
// in a similar way as github
|
||||
|
|
|
@ -34,6 +34,7 @@ func TestGenericPrincipalID(t *testing.T) {
|
|||
prov := oauth2.Generic{
|
||||
Logger: logger,
|
||||
APIURL: mockAPI.URL,
|
||||
APIKey: "email",
|
||||
}
|
||||
tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport)
|
||||
if err != nil {
|
||||
|
|
|
@ -31,10 +31,12 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
|
|||
cells := make([]dashboardCellResponse, len(dcells))
|
||||
for i, cell := range dcells {
|
||||
newCell := chronograf.DashboardCell{}
|
||||
|
||||
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
|
||||
copy(newCell.Queries, cell.Queries)
|
||||
|
||||
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
|
||||
copy(newCell.CellColors, cell.CellColors)
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
labels := []string{"x", "y", "y2"}
|
||||
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
|
||||
|
@ -70,8 +72,22 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
|
|||
// ValidDashboardCellRequest verifies that the dashboard cells have a query and
|
||||
// have the correct axes specified
|
||||
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("Chronograf dashboard cell was nil")
|
||||
}
|
||||
|
||||
CorrectWidthHeight(c)
|
||||
return HasCorrectAxes(c)
|
||||
for _, q := range c.Queries {
|
||||
if err := ValidateQueryConfig(&q.QueryConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
MoveTimeShift(c)
|
||||
err := HasCorrectAxes(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return HasCorrectColors(c)
|
||||
}
|
||||
|
||||
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
|
||||
|
@ -93,6 +109,19 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// HasCorrectColors verifies that the format of each color is correct
|
||||
func HasCorrectColors(c *chronograf.DashboardCell) error {
|
||||
for _, color := range c.CellColors {
|
||||
if !oneOf(color.Type, "max", "min", "threshold") {
|
||||
return chronograf.ErrInvalidColorType
|
||||
}
|
||||
if len(color.Hex) != 7 {
|
||||
return chronograf.ErrInvalidColor
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// oneOf reports whether a provided string is a member of a variadic list of
|
||||
// valid options
|
||||
func oneOf(prop string, validOpts ...string) bool {
|
||||
|
@ -115,12 +144,22 @@ func CorrectWidthHeight(c *chronograf.DashboardCell) {
|
|||
}
|
||||
}
|
||||
|
||||
// MoveTimeShift moves TimeShift from the QueryConfig to the DashboardQuery
|
||||
func MoveTimeShift(c *chronograf.DashboardCell) {
|
||||
for i, query := range c.Queries {
|
||||
query.Shifts = query.QueryConfig.Shifts
|
||||
c.Queries[i] = query
|
||||
}
|
||||
}
|
||||
|
||||
// AddQueryConfig updates a cell by converting InfluxQL into queryconfigs
|
||||
// If influxql cannot be represented by a full query config, then, the
|
||||
// query config's raw text is set to the command.
|
||||
func AddQueryConfig(c *chronograf.DashboardCell) {
|
||||
for i, q := range c.Queries {
|
||||
qc := ToQueryConfig(q.Command)
|
||||
qc.Shifts = append([]chronograf.TimeShift(nil), q.Shifts...)
|
||||
q.Shifts = nil
|
||||
q.QueryConfig = qc
|
||||
c.Queries[i] = q
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
"correct axes",
|
||||
&chronograf.DashboardCell{
|
||||
name: "correct axes",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{"0", "100"},
|
||||
|
@ -39,11 +39,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid axes present",
|
||||
&chronograf.DashboardCell{
|
||||
name: "invalid axes present",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"axis of evil": chronograf.Axis{
|
||||
Bounds: []string{"666", "666"},
|
||||
|
@ -53,11 +52,11 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
"linear scale value",
|
||||
&chronograf.DashboardCell{
|
||||
name: "linear scale value",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Scale: "linear",
|
||||
|
@ -65,11 +64,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"log scale value",
|
||||
&chronograf.DashboardCell{
|
||||
name: "log scale value",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Scale: "log",
|
||||
|
@ -77,11 +75,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid scale value",
|
||||
&chronograf.DashboardCell{
|
||||
name: "invalid scale value",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Scale: "potatoes",
|
||||
|
@ -89,11 +86,11 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
"base 10 axis",
|
||||
&chronograf.DashboardCell{
|
||||
name: "base 10 axis",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Base: "10",
|
||||
|
@ -101,11 +98,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"base 2 axis",
|
||||
&chronograf.DashboardCell{
|
||||
name: "base 2 axis",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Base: "2",
|
||||
|
@ -113,11 +109,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid base",
|
||||
&chronograf.DashboardCell{
|
||||
name: "invalid base",
|
||||
cell: &chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Base: "all your base are belong to us",
|
||||
|
@ -125,7 +120,7 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -150,26 +145,26 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
expectedCode int
|
||||
}{
|
||||
{
|
||||
"happy path",
|
||||
&url.URL{
|
||||
name: "happy path",
|
||||
reqURL: &url.URL{
|
||||
Path: "/chronograf/v1/dashboards/1/cells",
|
||||
},
|
||||
map[string]string{
|
||||
ctxParams: map[string]string{
|
||||
"id": "1",
|
||||
},
|
||||
[]chronograf.DashboardCell{},
|
||||
[]chronograf.DashboardCell{},
|
||||
http.StatusOK,
|
||||
mockResponse: []chronograf.DashboardCell{},
|
||||
expected: []chronograf.DashboardCell{},
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
"cell axes should always be \"x\", \"y\", and \"y2\"",
|
||||
&url.URL{
|
||||
name: "cell axes should always be \"x\", \"y\", and \"y2\"",
|
||||
reqURL: &url.URL{
|
||||
Path: "/chronograf/v1/dashboards/1/cells",
|
||||
},
|
||||
map[string]string{
|
||||
ctxParams: map[string]string{
|
||||
"id": "1",
|
||||
},
|
||||
[]chronograf.DashboardCell{
|
||||
mockResponse: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
|
||||
X: 0,
|
||||
|
@ -182,16 +177,17 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
Axes: map[string]chronograf.Axis{},
|
||||
},
|
||||
},
|
||||
[]chronograf.DashboardCell{
|
||||
expected: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "CPU",
|
||||
Type: "bar",
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "CPU",
|
||||
Type: "bar",
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
|
@ -205,7 +201,7 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
http.StatusOK,
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -217,7 +213,10 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
params := httprouter.Params{}
|
||||
for k, v := range test.ctxParams {
|
||||
params = append(params, httprouter.Param{k, v})
|
||||
params = append(params, httprouter.Param{
|
||||
Key: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
ctx = httprouter.WithParams(ctx, params)
|
||||
|
||||
|
@ -275,3 +274,76 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasCorrectColors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c *chronograf.DashboardCell
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "min type is valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
Type: "min",
|
||||
Hex: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max type is valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
Type: "max",
|
||||
Hex: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "threshold type is valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
Type: "threshold",
|
||||
Hex: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid color type",
|
||||
c: &chronograf.DashboardCell{
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
Type: "unknown",
|
||||
Hex: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid color hex",
|
||||
c: &chronograf.DashboardCell{
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
Type: "min",
|
||||
Hex: "bad",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := server.HasCorrectColors(tt.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("HasCorrectColors() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,6 +219,13 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
{
|
||||
Source: "/chronograf/v1/sources/1",
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'",
|
||||
Shifts: []chronograf.TimeShift{
|
||||
{
|
||||
Label: "Best Week Evar",
|
||||
Unit: "d",
|
||||
Quantity: "7",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
|
@ -267,9 +274,17 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
Tags: make(map[string][]string, 0),
|
||||
AreTagsAccepted: false,
|
||||
Shifts: []chronograf.TimeShift{
|
||||
{
|
||||
Label: "Best Week Evar",
|
||||
Unit: "d",
|
||||
Quantity: "7",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{"0", "100"},
|
||||
|
@ -303,6 +318,7 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m",
|
||||
|
|
|
@ -229,12 +229,12 @@ func Test_KapacitorRulesGet(t *testing.T) {
|
|||
bg := context.Background()
|
||||
params := httprouter.Params{
|
||||
{
|
||||
"id",
|
||||
"1",
|
||||
Key: "id",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
"kid",
|
||||
"1",
|
||||
Key: "kid",
|
||||
Value: "1",
|
||||
},
|
||||
}
|
||||
ctx := httprouter.WithParams(bg, params)
|
||||
|
@ -260,8 +260,8 @@ func Test_KapacitorRulesGet(t *testing.T) {
|
|||
|
||||
actual := make([]chronograf.AlertRule, len(frame.Rules))
|
||||
|
||||
for idx, _ := range frame.Rules {
|
||||
actual[idx] = frame.Rules[idx].AlertRule
|
||||
for i := range frame.Rules {
|
||||
actual[i] = frame.Rules[i].AlertRule
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
|
|
|
@ -202,7 +202,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
// Encapsulate the router with OAuth2
|
||||
var auth http.Handler
|
||||
auth, allRoutes.AuthRoutes = AuthAPI(opts, router)
|
||||
allRoutes.LogoutLink = "/oauth/logout"
|
||||
allRoutes.LogoutLink = path.Join(opts.Basepath, "/oauth/logout")
|
||||
|
||||
// Create middleware that redirects to the appropriate provider logout
|
||||
router.GET(allRoutes.LogoutLink, Logout("/", basepath, allRoutes.AuthRoutes))
|
||||
|
|
|
@ -84,6 +84,7 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
|
|||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
qc.Shifts = []chronograf.TimeShift{}
|
||||
qr.QueryConfig = qc
|
||||
|
||||
if stmt, err := queries.ParseSelect(query); err == nil {
|
||||
|
|
|
@ -60,7 +60,7 @@ func TestService_Queries(t *testing.T) {
|
|||
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
|
||||
}
|
||||
]}`))),
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"}},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]}
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"},"shifts":[]},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -81,7 +81,7 @@ func TestService_Queries(t *testing.T) {
|
|||
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
|
||||
}
|
||||
]}`))),
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null}}]}
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null,"shifts":[]}}]}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -166,7 +166,7 @@ func TestService_Queries(t *testing.T) {
|
|||
}
|
||||
]
|
||||
}`))),
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","range":null},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 15m AND time \u003c now() GROUP BY time(2s)","tempVars":[{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":interval:","values":[{"value":"1000","type":"resolution","selected":false},{"value":"3","type":"pointsPerPixel","selected":false}]}]}]}
|
||||
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","range":null,"shifts":[]},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 15m AND time \u003c now() GROUP BY time(2s)","tempVars":[{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":interval:","values":[{"value":"1000","type":"resolution","selected":false},{"value":"3","type":"pointsPerPixel","selected":false}]}]}]}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
@ -22,3 +24,28 @@ func ToQueryConfig(query string) chronograf.QueryConfig {
|
|||
Tags: make(map[string][]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
var validFieldTypes = map[string]bool{
|
||||
"func": true,
|
||||
"field": true,
|
||||
"integer": true,
|
||||
"number": true,
|
||||
"regex": true,
|
||||
"wildcard": true,
|
||||
}
|
||||
|
||||
// ValidateQueryConfig checks any query config input
|
||||
func ValidateQueryConfig(q *chronograf.QueryConfig) error {
|
||||
for _, fld := range q.Fields {
|
||||
invalid := fmt.Errorf(`invalid field type "%s" ; expect func, field, integer, number, regex, wildcard`, fld.Type)
|
||||
if !validFieldTypes[fld.Type] {
|
||||
return invalid
|
||||
}
|
||||
for _, arg := range fld.Args {
|
||||
if !validFieldTypes[arg.Type] {
|
||||
return invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestValidateQueryConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
q *chronograf.QueryConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "invalid field type",
|
||||
q: &chronograf.QueryConfig{
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Type: "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid field args",
|
||||
q: &chronograf.QueryConfig{
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Type: "func",
|
||||
Args: []chronograf.Field{
|
||||
{
|
||||
Type: "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateQueryConfig(tt.q); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateQueryConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ type Server struct {
|
|||
GenericAuthURL string `long:"generic-auth-url" description:"OAuth 2.0 provider's authorization endpoint URL" env:"GENERIC_AUTH_URL"`
|
||||
GenericTokenURL string `long:"generic-token-url" description:"OAuth 2.0 provider's token endpoint URL" env:"GENERIC_TOKEN_URL"`
|
||||
GenericAPIURL string `long:"generic-api-url" description:"URL that returns OpenID UserInfo compatible information." env:"GENERIC_API_URL"`
|
||||
GenericAPIKey string `long:"generic-api-key" description:"JSON lookup key into OpenID UserInfo. (Azure should be userPrincipalName)" default:"email" env:"GENERIC_API_KEY"`
|
||||
|
||||
Auth0Domain string `long:"auth0-domain" description:"Subdomain of auth0.com used for Auth0 OAuth2 authentication" env:"AUTH0_DOMAIN"`
|
||||
Auth0ClientID string `long:"auth0-client-id" description:"Auth0 Client ID for OAuth2 support" env:"AUTH0_CLIENT_ID"`
|
||||
|
@ -181,6 +182,7 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato
|
|||
AuthURL: s.GenericAuthURL,
|
||||
TokenURL: s.GenericTokenURL,
|
||||
APIURL: s.GenericAPIURL,
|
||||
APIKey: s.GenericAPIKey,
|
||||
Logger: logger,
|
||||
}
|
||||
jwt := oauth2.NewJWT(s.TokenSecret)
|
||||
|
|
|
@ -51,7 +51,7 @@ func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chr
|
|||
}
|
||||
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
|
||||
tls := strings.Contains(src.MetaURL, "https")
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, src.Username, src.Password, tls, client)
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, client)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
@ -53,7 +53,10 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
|
|||
},
|
||||
}
|
||||
|
||||
if src.Type == chronograf.InfluxEnterprise {
|
||||
// MetaURL is currently a string, but eventually, we'd like to change it
|
||||
// to a slice. Checking len(src.MetaURL) is functionally equivalent to
|
||||
// checking if it is equal to the empty string.
|
||||
if src.Type == chronograf.InfluxEnterprise && len(src.MetaURL) != 0 {
|
||||
res.Links.Roles = fmt.Sprintf("%s/%d/roles", httpAPISrcs, src.ID)
|
||||
}
|
||||
return res
|
||||
|
@ -243,7 +246,9 @@ func (h *Service) UpdateSource(w http.ResponseWriter, r *http.Request) {
|
|||
if req.URL != "" {
|
||||
src.URL = req.URL
|
||||
}
|
||||
if req.MetaURL != "" {
|
||||
// If the supplied MetaURL is different from the
|
||||
// one supplied on the request, update the value
|
||||
if req.MetaURL != src.MetaURL {
|
||||
src.MetaURL = req.MetaURL
|
||||
}
|
||||
if req.Type != "" {
|
||||
|
|
|
@ -550,6 +550,7 @@
|
|||
"patch": {
|
||||
"tags": ["sources", "users"],
|
||||
"summary": "Update user configuration",
|
||||
"description": "Update one parameter at a time (one of password, permissions or roles)",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -3955,6 +3956,14 @@
|
|||
],
|
||||
"default": "line"
|
||||
},
|
||||
"colors": {
|
||||
"description":
|
||||
"Colors define encoding data into a visualization",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/DashboardColor"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -4025,6 +4034,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"DashboardColor": {
|
||||
"type": "object",
|
||||
"description":
|
||||
"Color defines an encoding of a data value into color space",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "ID is the unique id of the cell color",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is how the color is used.",
|
||||
"type": "string",
|
||||
"enum": ["min", "max", "threshold"]
|
||||
},
|
||||
"hex": {
|
||||
"description": "Hex is the hex number of the color",
|
||||
"type": "string",
|
||||
"maxLength": 7,
|
||||
"minLength": 7
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the user-facing name of the hex color",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Value is the data value mapped to this color",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Axis": {
|
||||
"type": "object",
|
||||
"description": "A description of a particular axis for a visualization",
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
'arrow-parens': 0,
|
||||
'comma-dangle': [2, 'always-multiline'],
|
||||
'no-cond-assign': 2,
|
||||
'no-console': ['error', {allow: ['error']}],
|
||||
'no-console': ['error', {allow: ['error', 'warn']}],
|
||||
'no-constant-condition': 2,
|
||||
'no-control-regex': 2,
|
||||
'no-debugger': 2,
|
||||
|
|
|
@ -11,14 +11,14 @@
|
|||
"scripts": {
|
||||
"build": "yarn run clean && env NODE_ENV=production webpack --optimize-minimize --config ./webpack/prodConfig.js",
|
||||
"build:dev": "webpack --config ./webpack/devConfig.js",
|
||||
"start": "webpack --watch --config ./webpack/devConfig.js",
|
||||
"start": "yarn run clean && webpack --watch --config ./webpack/devConfig.js",
|
||||
"start:hmr": "webpack-dev-server --open --config ./webpack/devConfig.js",
|
||||
"lint": "esw src/",
|
||||
"test": "karma start",
|
||||
"test:integration": "nightwatch tests --skip",
|
||||
"test:lint": "yarn run lint; yarn run test",
|
||||
"test:dev": "concurrently \"yarn run lint -- --watch\" \"yarn run test -- --no-single-run --reporters=verbose\"",
|
||||
"clean": "rm -rf build",
|
||||
"test:dev": "concurrently \"yarn run lint --watch\" \"yarn run test --no-single-run --reporters=verbose\"",
|
||||
"clean": "rm -rf build/*",
|
||||
"storybook": "node ./storybook.js",
|
||||
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
|
||||
},
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import reducer from 'src/data_explorer/reducers/queryConfigs'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
chooseTag,
|
||||
groupByTag,
|
||||
groupByTime,
|
||||
|
@ -26,63 +28,63 @@ const fakeAddQueryAction = (panelID, queryID) => {
|
|||
}
|
||||
}
|
||||
|
||||
function buildInitialState(queryId, params) {
|
||||
return Object.assign({}, defaultQueryConfig({id: queryId}), params)
|
||||
function buildInitialState(queryID, params) {
|
||||
return Object.assign({}, defaultQueryConfig({id: queryID}), params)
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
||||
const queryId = 123
|
||||
const queryID = 123
|
||||
|
||||
it('can add a query', () => {
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryId))
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryID))
|
||||
|
||||
const actual = state[queryId]
|
||||
const expected = defaultQueryConfig({id: queryId})
|
||||
const actual = state[queryID]
|
||||
const expected = defaultQueryConfig({id: queryID})
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
describe('choosing db, rp, and measurement', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
state = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: 'telegraf',
|
||||
retentionPolicy: 'monitor',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].database).to.equal('telegraf')
|
||||
expect(newState[queryId].retentionPolicy).to.equal('monitor')
|
||||
expect(newState[queryID].database).to.equal('telegraf')
|
||||
expect(newState[queryID].retentionPolicy).to.equal('monitor')
|
||||
})
|
||||
|
||||
it('sets the measurement', () => {
|
||||
const newState = reducer(state, chooseMeasurement(queryId, 'mem'))
|
||||
const newState = reducer(state, chooseMeasurement(queryID, 'mem'))
|
||||
|
||||
expect(newState[queryId].measurement).to.equal('mem')
|
||||
expect(newState[queryID].measurement).to.equal('mem')
|
||||
})
|
||||
})
|
||||
|
||||
describe('a query has measurements and fields', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: '_internal',
|
||||
retentionPolicy: 'daily',
|
||||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryId, 'disk'))
|
||||
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
|
||||
|
||||
state = reducer(
|
||||
three,
|
||||
addInitialField(queryId, {
|
||||
addInitialField(queryID, {
|
||||
value: 'a great field',
|
||||
type: 'field',
|
||||
})
|
||||
|
@ -92,91 +94,91 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('choosing a new namespace', () => {
|
||||
it('clears out the old measurement and fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].measurement).to.equal('disk')
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].measurement).to.equal('disk')
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: 'newdb',
|
||||
retentionPolicy: 'newrp',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].measurement).to.be.null
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
expect(newState[queryID].measurement).to.be.null
|
||||
expect(newState[queryID].fields.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('choosing a new measurement', () => {
|
||||
it('leaves the namespace and clears out the old fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseMeasurement(queryId, 'newmeasurement')
|
||||
chooseMeasurement(queryID, 'newmeasurement')
|
||||
)
|
||||
|
||||
expect(state[queryId].database).to.equal(newState[queryId].database)
|
||||
expect(state[queryId].retentionPolicy).to.equal(
|
||||
newState[queryId].retentionPolicy
|
||||
expect(state[queryID].database).to.equal(newState[queryID].database)
|
||||
expect(state[queryID].retentionPolicy).to.equal(
|
||||
newState[queryID].retentionPolicy
|
||||
)
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
expect(newState[queryID].fields.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_TOGGLE_FIELD', () => {
|
||||
it('can toggle multiple fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {
|
||||
toggleField(queryID, {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(2)
|
||||
expect(newState[queryId].fields[1].alias).to.deep.equal('mean_f2')
|
||||
expect(newState[queryId].fields[1].args).to.deep.equal([
|
||||
expect(newState[queryID].fields.length).to.equal(2)
|
||||
expect(newState[queryID].fields[1].alias).to.deep.equal('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).to.deep.equal([
|
||||
{value: 'f2', type: 'field'},
|
||||
])
|
||||
expect(newState[queryId].fields[1].value).to.deep.equal('mean')
|
||||
expect(newState[queryID].fields[1].value).to.deep.equal('mean')
|
||||
})
|
||||
|
||||
it('applies a func to newly selected fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryId].fields[0].type).to.equal('func')
|
||||
expect(state[queryId].fields[0].value).to.equal('mean')
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields[0].type).to.equal('func')
|
||||
expect(state[queryID].fields[0].value).to.equal('mean')
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {
|
||||
toggleField(queryID, {
|
||||
value: 'f2',
|
||||
type: 'field',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields[1].value).to.equal('mean')
|
||||
expect(newState[queryId].fields[1].alias).to.equal('mean_f2')
|
||||
expect(newState[queryId].fields[1].args).to.deep.equal([
|
||||
expect(newState[queryID].fields[1].value).to.equal('mean')
|
||||
expect(newState[queryID].fields[1].alias).to.equal('mean_f2')
|
||||
expect(newState[queryID].fields[1].args).to.deep.equal([
|
||||
{value: 'f2', type: 'field'},
|
||||
])
|
||||
expect(newState[queryId].fields[1].type).to.equal('func')
|
||||
expect(newState[queryID].fields[1].type).to.equal('func')
|
||||
})
|
||||
|
||||
it('adds the field property to query config if not found', () => {
|
||||
delete state[queryId].fields
|
||||
expect(state[queryId].fields).to.equal(undefined)
|
||||
delete state[queryID].fields
|
||||
expect(state[queryID].fields).to.equal(undefined)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {value: 'fk1', type: 'field'})
|
||||
toggleField(queryID, {value: 'fk1', type: 'field'})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryID].fields.length).to.equal(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -189,7 +191,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const f4 = {value: 'f4', type: 'field'}
|
||||
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -201,7 +203,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
},
|
||||
}
|
||||
|
||||
const action = applyFuncsToField(queryId, {
|
||||
const action = applyFuncsToField(queryID, {
|
||||
field: {value: 'f1', type: 'field'},
|
||||
funcs: [
|
||||
{value: 'fn3', type: 'func', args: []},
|
||||
|
@ -211,7 +213,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fields).to.deep.equal([
|
||||
expect(nextState[queryID].fields).to.deep.equal([
|
||||
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
|
||||
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
|
||||
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
|
||||
|
@ -230,7 +232,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const groupBy = {time: '1m', tags: []}
|
||||
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -239,35 +241,35 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
},
|
||||
}
|
||||
|
||||
const action = removeFuncs(queryId, fields, groupBy)
|
||||
const action = removeFuncs(queryID, fields, groupBy)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
const actual = nextState[queryId].fields
|
||||
const actual = nextState[queryID].fields
|
||||
const expected = [f1, f2]
|
||||
|
||||
expect(actual).to.eql(expected)
|
||||
expect(nextState[queryId].groupBy.time).to.equal(null)
|
||||
expect(nextState[queryID].groupBy.time).to.equal(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_CHOOSE_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {
|
||||
k1: ['v0'],
|
||||
k2: ['foo'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
expect(nextState[queryID].tags).to.eql({
|
||||
k1: ['v0', 'v1'],
|
||||
k2: ['foo'],
|
||||
})
|
||||
|
@ -275,31 +277,31 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
it("creates a new entry if it's the first key", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
expect(nextState[queryID].tags).to.eql({
|
||||
k1: ['v1'],
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a value that is already in the list', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {
|
||||
k1: ['v1'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
@ -307,14 +309,14 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
const nextState = reducer(initialState, action)
|
||||
|
||||
// TODO: this should probably remove the `k1` property entirely from the tags object
|
||||
expect(nextState[queryId].tags).to.eql({})
|
||||
expect(nextState[queryID].tags).to.eql({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -323,11 +325,11 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
groupBy: {tags: [], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
expect(nextState[queryID].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: ['k1'],
|
||||
})
|
||||
|
@ -335,7 +337,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
|
||||
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -344,11 +346,11 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
groupBy: {tags: ['k1'], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
expect(nextState[queryID].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: [],
|
||||
})
|
||||
|
@ -358,14 +360,14 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
describe('DE_TOGGLE_TAG_ACCEPTANCE', () => {
|
||||
it('it toggles areTagsAccepted', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const action = toggleTagAcceptance(queryId)
|
||||
const action = toggleTagAcceptance(queryID)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].areTagsAccepted).to.equal(
|
||||
!initialState[queryId].areTagsAccepted
|
||||
expect(nextState[queryID].areTagsAccepted).to.equal(
|
||||
!initialState[queryID].areTagsAccepted
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -374,99 +376,113 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
|||
it('applys the appropriate group by time', () => {
|
||||
const time = '100y'
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const action = groupByTime(queryId, time)
|
||||
const action = groupByTime(queryID, time)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy.time).to.equal(time)
|
||||
expect(nextState[queryID].groupBy.time).to.equal(time)
|
||||
})
|
||||
})
|
||||
|
||||
it('updates entire config', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const expected = defaultQueryConfig({id: queryId}, {rawText: 'hello'})
|
||||
const expected = defaultQueryConfig({id: queryID}, {rawText: 'hello'})
|
||||
const action = updateQueryConfig(expected)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId]).to.deep.equal(expected)
|
||||
expect(nextState[queryID]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it("updates a query's raw text", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const text = 'foo'
|
||||
const action = updateRawQuery(queryId, text)
|
||||
const action = updateRawQuery(queryID, text)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].rawText).to.equal('foo')
|
||||
expect(nextState[queryID].rawText).to.equal('foo')
|
||||
})
|
||||
|
||||
it("updates a query's raw status", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const status = 'your query was sweet'
|
||||
const action = editQueryStatus(queryId, status)
|
||||
const action = editQueryStatus(queryID, status)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].status).to.equal(status)
|
||||
expect(nextState[queryID].status).to.equal(status)
|
||||
})
|
||||
|
||||
describe('DE_FILL', () => {
|
||||
it('applies an explicit fill when group by time is used', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const time = '10s'
|
||||
const action = groupByTime(queryId, time)
|
||||
const action = groupByTime(queryID, time)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fill).to.equal(NULL_STRING)
|
||||
expect(nextState[queryID].fill).to.equal(NULL_STRING)
|
||||
})
|
||||
|
||||
it('updates fill to non-null-string non-number string value', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const action = fill(queryId, LINEAR)
|
||||
const action = fill(queryID, LINEAR)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fill).to.equal(LINEAR)
|
||||
expect(nextState[queryID].fill).to.equal(LINEAR)
|
||||
})
|
||||
|
||||
it('updates fill to string integer value', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const INT_STRING = '1337'
|
||||
const action = fill(queryId, INT_STRING)
|
||||
const action = fill(queryID, INT_STRING)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fill).to.equal(INT_STRING)
|
||||
expect(nextState[queryID].fill).to.equal(INT_STRING)
|
||||
})
|
||||
|
||||
it('updates fill to string float value', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const FLOAT_STRING = '1.337'
|
||||
const action = fill(queryId, FLOAT_STRING)
|
||||
const action = fill(queryID, FLOAT_STRING)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fill).to.equal(FLOAT_STRING)
|
||||
expect(nextState[queryID].fill).to.equal(FLOAT_STRING)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DE_TIME_SHIFT', () => {
|
||||
it('can shift the time', () => {
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const shift = {quantity: 1, unit: 'd', duration: '1d'}
|
||||
const action = timeShift(queryID, shift)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].shifts).to.deep.equal([shift])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import reducer from 'src/kapacitor/reducers/queryConfigs'
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
chooseTag,
|
||||
timeShift,
|
||||
groupByTag,
|
||||
toggleField,
|
||||
groupByTime,
|
||||
chooseNamespace,
|
||||
chooseMeasurement,
|
||||
chooseTag,
|
||||
groupByTag,
|
||||
toggleTagAcceptance,
|
||||
toggleField,
|
||||
applyFuncsToField,
|
||||
groupByTime,
|
||||
toggleTagAcceptance,
|
||||
} from 'src/kapacitor/actions/queryConfigs'
|
||||
|
||||
const fakeAddQueryAction = (panelID, queryID) => {
|
||||
|
@ -18,142 +19,142 @@ const fakeAddQueryAction = (panelID, queryID) => {
|
|||
}
|
||||
}
|
||||
|
||||
function buildInitialState(queryId, params) {
|
||||
function buildInitialState(queryID, params) {
|
||||
return Object.assign(
|
||||
{},
|
||||
defaultQueryConfig({id: queryId, isKapacitorRule: true}),
|
||||
defaultQueryConfig({id: queryID, isKapacitorRule: true}),
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
||||
const queryId = 123
|
||||
const queryID = 123
|
||||
|
||||
it('can add a query', () => {
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryId))
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryID))
|
||||
|
||||
const actual = state[queryId]
|
||||
const expected = defaultQueryConfig({id: queryId, isKapacitorRule: true})
|
||||
const actual = state[queryID]
|
||||
const expected = defaultQueryConfig({id: queryID, isKapacitorRule: true})
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
describe('choosing db, rp, and measurement', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
state = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: 'telegraf',
|
||||
retentionPolicy: 'monitor',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].database).to.equal('telegraf')
|
||||
expect(newState[queryId].retentionPolicy).to.equal('monitor')
|
||||
expect(newState[queryID].database).to.equal('telegraf')
|
||||
expect(newState[queryID].retentionPolicy).to.equal('monitor')
|
||||
})
|
||||
|
||||
it('sets the measurement', () => {
|
||||
const newState = reducer(state, chooseMeasurement(queryId, 'mem'))
|
||||
const newState = reducer(state, chooseMeasurement(queryID, 'mem'))
|
||||
|
||||
expect(newState[queryId].measurement).to.equal('mem')
|
||||
expect(newState[queryID].measurement).to.equal('mem')
|
||||
})
|
||||
})
|
||||
|
||||
describe('a query has measurements and fields', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryID))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: '_internal',
|
||||
retentionPolicy: 'daily',
|
||||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryId, 'disk'))
|
||||
const three = reducer(two, chooseMeasurement(queryID, 'disk'))
|
||||
state = reducer(
|
||||
three,
|
||||
toggleField(queryId, {value: 'a great field', funcs: []})
|
||||
toggleField(queryID, {value: 'a great field', funcs: []})
|
||||
)
|
||||
})
|
||||
|
||||
describe('choosing a new namespace', () => {
|
||||
it('clears out the old measurement and fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].measurement).to.exist
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].measurement).to.exist
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
chooseNamespace(queryID, {
|
||||
database: 'newdb',
|
||||
retentionPolicy: 'newrp',
|
||||
})
|
||||
)
|
||||
|
||||
expect(newState[queryId].measurement).not.to.exist
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
expect(newState[queryID].measurement).not.to.exist
|
||||
expect(newState[queryID].fields.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('choosing a new measurement', () => {
|
||||
it('leaves the namespace and clears out the old fields', () => {
|
||||
// what about tags?
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseMeasurement(queryId, 'newmeasurement')
|
||||
chooseMeasurement(queryID, 'newmeasurement')
|
||||
)
|
||||
|
||||
expect(state[queryId].database).to.equal(newState[queryId].database)
|
||||
expect(state[queryId].retentionPolicy).to.equal(
|
||||
newState[queryId].retentionPolicy
|
||||
expect(state[queryID].database).to.equal(newState[queryID].database)
|
||||
expect(state[queryID].retentionPolicy).to.equal(
|
||||
newState[queryID].retentionPolicy
|
||||
)
|
||||
expect(newState[queryId].fields.length).to.equal(0)
|
||||
expect(newState[queryID].fields.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the query is part of a kapacitor rule', () => {
|
||||
it('only allows one field', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {value: 'a different field', type: 'field'})
|
||||
toggleField(queryID, {value: 'a different field', type: 'field'})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].value).to.equal('a different field')
|
||||
expect(newState[queryID].fields.length).to.equal(1)
|
||||
expect(newState[queryID].fields[0].value).to.equal('a different field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_TOGGLE_FIELD', () => {
|
||||
it('cannot toggle multiple fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {value: 'a different field', type: 'field'})
|
||||
toggleField(queryID, {value: 'a different field', type: 'field'})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].value).to.equal('a different field')
|
||||
expect(newState[queryID].fields.length).to.equal(1)
|
||||
expect(newState[queryID].fields[0].value).to.equal('a different field')
|
||||
})
|
||||
|
||||
it('applies no funcs to newly selected fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
expect(state[queryID].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {value: 'a different field', type: 'field'})
|
||||
toggleField(queryID, {value: 'a different field', type: 'field'})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields[0].type).to.equal('field')
|
||||
expect(newState[queryID].fields[0].type).to.equal('field')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -162,7 +163,7 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
it('applies functions to a field without any existing functions', () => {
|
||||
const f1 = {value: 'f1', type: 'field'}
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -174,13 +175,13 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
},
|
||||
}
|
||||
|
||||
const action = applyFuncsToField(queryId, {
|
||||
const action = applyFuncsToField(queryID, {
|
||||
field: {value: 'f1', type: 'field'},
|
||||
funcs: [{value: 'fn3', type: 'func'}, {value: 'fn4', type: 'func'}],
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
const actual = nextState[queryId].fields
|
||||
const actual = nextState[queryID].fields
|
||||
const expected = [
|
||||
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
|
||||
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
|
||||
|
@ -193,21 +194,21 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
describe('KAPA_CHOOSE_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {
|
||||
k1: ['v0'],
|
||||
k2: ['foo'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
expect(nextState[queryID].tags).to.eql({
|
||||
k1: ['v0', 'v1'],
|
||||
k2: ['foo'],
|
||||
})
|
||||
|
@ -215,31 +216,31 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
|
||||
it("creates a new entry if it's the first key", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
expect(nextState[queryID].tags).to.eql({
|
||||
k1: ['v1'],
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a value that is already in the list', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
[queryID]: buildInitialState(queryID, {
|
||||
tags: {
|
||||
k1: ['v1'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
const action = chooseTag(queryID, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
@ -247,14 +248,14 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
const nextState = reducer(initialState, action)
|
||||
|
||||
// TODO: this should probably remove the `k1` property entirely from the tags object
|
||||
expect(nextState[queryId].tags).to.eql({})
|
||||
expect(nextState[queryID].tags).to.eql({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -263,11 +264,11 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
groupBy: {tags: [], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
expect(nextState[queryID].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: ['k1'],
|
||||
})
|
||||
|
@ -275,7 +276,7 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
|
||||
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
[queryID]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
|
@ -284,11 +285,11 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
groupBy: {tags: ['k1'], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
const action = groupByTag(queryID, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
expect(nextState[queryID].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: [],
|
||||
})
|
||||
|
@ -298,14 +299,14 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
describe('KAPA_TOGGLE_TAG_ACCEPTANCE', () => {
|
||||
it('it toggles areTagsAccepted', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
const action = toggleTagAcceptance(queryId)
|
||||
const action = toggleTagAcceptance(queryID)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].areTagsAccepted).to.equal(
|
||||
!initialState[queryId].areTagsAccepted
|
||||
expect(nextState[queryID].areTagsAccepted).to.equal(
|
||||
!initialState[queryID].areTagsAccepted
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -314,14 +315,28 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
|||
it('applys the appropriate group by time', () => {
|
||||
const time = '100y'
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const action = groupByTime(queryId, time)
|
||||
const action = groupByTime(queryID, time)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy.time).to.equal(time)
|
||||
expect(nextState[queryID].groupBy.time).to.equal(time)
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_TIME_SHIFT', () => {
|
||||
it('can shift the time', () => {
|
||||
const initialState = {
|
||||
[queryID]: buildInitialState(queryID),
|
||||
}
|
||||
|
||||
const shift = {quantity: 1, unit: 'd', duration: '1d'}
|
||||
const action = timeShift(queryID, shift)
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryID].shifts).to.deep.equal([shift])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import {buildRoles, buildClusterAccounts} from 'shared/presenters'
|
||||
import {
|
||||
buildRoles,
|
||||
buildClusterAccounts,
|
||||
buildDefaultYLabel,
|
||||
} from 'shared/presenters'
|
||||
import defaultQueryConfig from 'utils/defaultQueryConfig'
|
||||
|
||||
describe('Presenters', function() {
|
||||
describe('roles utils', function() {
|
||||
describe('buildRoles', function() {
|
||||
describe('when a role has no users', function() {
|
||||
it("sets a role's users as an empty array", function() {
|
||||
describe('Presenters', () => {
|
||||
describe('roles utils', () => {
|
||||
describe('buildRoles', () => {
|
||||
describe('when a role has no users', () => {
|
||||
it("sets a role's users as an empty array", () => {
|
||||
const roles = [
|
||||
{
|
||||
name: 'Marketing',
|
||||
|
@ -20,8 +25,8 @@ describe('Presenters', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('when a role has no permissions', function() {
|
||||
it("set's a roles permission as an empty array", function() {
|
||||
describe('when a role has no permissions', () => {
|
||||
it("set's a roles permission as an empty array", () => {
|
||||
const roles = [
|
||||
{
|
||||
name: 'Marketing',
|
||||
|
@ -35,9 +40,10 @@ describe('Presenters', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('when a role has users and permissions', function() {
|
||||
beforeEach(function() {
|
||||
const roles = [
|
||||
describe('when a role has users and permissions', () => {
|
||||
let roles
|
||||
beforeEach(() => {
|
||||
const rs = [
|
||||
{
|
||||
name: 'Marketing',
|
||||
permissions: {
|
||||
|
@ -49,18 +55,18 @@ describe('Presenters', function() {
|
|||
},
|
||||
]
|
||||
|
||||
this.roles = buildRoles(roles)
|
||||
roles = buildRoles(rs)
|
||||
})
|
||||
|
||||
it('each role has a name and a list of users (if they exist)', function() {
|
||||
const role = this.roles[0]
|
||||
it('each role has a name and a list of users (if they exist)', () => {
|
||||
const role = roles[0]
|
||||
expect(role.name).to.equal('Marketing')
|
||||
expect(role.users).to.contain('roley@influxdb.com')
|
||||
expect(role.users).to.contain('will@influxdb.com')
|
||||
})
|
||||
|
||||
it('transforms permissions into a list of objects and each permission has a list of resources', function() {
|
||||
expect(this.roles[0].permissions).to.eql([
|
||||
it('transforms permissions into a list of objects and each permission has a list of resources', () => {
|
||||
expect(roles[0].permissions).to.eql([
|
||||
{
|
||||
name: 'ViewAdmin',
|
||||
displayName: 'View Admin',
|
||||
|
@ -85,10 +91,10 @@ describe('Presenters', function() {
|
|||
})
|
||||
})
|
||||
|
||||
describe('cluster utils', function() {
|
||||
describe('buildClusterAccounts', function() {
|
||||
describe('cluster utils', () => {
|
||||
describe('buildClusterAccounts', () => {
|
||||
// TODO: break down this test into smaller individual assertions.
|
||||
it('adds role information to each cluster account and parses permissions', function() {
|
||||
it('adds role information to each cluster account and parses permissions', () => {
|
||||
const users = [
|
||||
{
|
||||
name: 'jon@example.com',
|
||||
|
@ -192,7 +198,7 @@ describe('Presenters', function() {
|
|||
expect(actual).to.eql(expected)
|
||||
})
|
||||
|
||||
it('can handle empty results for users and roles', function() {
|
||||
it('can handle empty results for users and roles', () => {
|
||||
const users = undefined
|
||||
const roles = undefined
|
||||
|
||||
|
@ -201,7 +207,7 @@ describe('Presenters', function() {
|
|||
expect(actual).to.eql([])
|
||||
})
|
||||
|
||||
it('sets roles to an empty array if a user has no roles', function() {
|
||||
it('sets roles to an empty array if a user has no roles', () => {
|
||||
const users = [
|
||||
{
|
||||
name: 'ned@example.com',
|
||||
|
@ -216,4 +222,41 @@ describe('Presenters', function() {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildDefaultYLabel', () => {
|
||||
it('can return the correct string for field', () => {
|
||||
const query = defaultQueryConfig({id: 1})
|
||||
const fields = [{value: 'usage_system', type: 'field'}]
|
||||
const measurement = 'm1'
|
||||
const queryConfig = {...query, measurement, fields}
|
||||
const actual = buildDefaultYLabel(queryConfig)
|
||||
|
||||
expect(actual).to.equal('m1.usage_system')
|
||||
})
|
||||
|
||||
it('can return the correct string for funcs with args', () => {
|
||||
const query = defaultQueryConfig({id: 1})
|
||||
const field = {value: 'usage_system', type: 'field'}
|
||||
const args = {
|
||||
value: 'mean',
|
||||
type: 'func',
|
||||
args: [field],
|
||||
alias: '',
|
||||
}
|
||||
|
||||
const f1 = {
|
||||
value: 'derivative',
|
||||
type: 'func',
|
||||
args: [args],
|
||||
alias: '',
|
||||
}
|
||||
|
||||
const fields = [f1]
|
||||
const measurement = 'm1'
|
||||
const queryConfig = {...query, measurement, fields}
|
||||
const actual = buildDefaultYLabel(queryConfig)
|
||||
|
||||
expect(actual).to.equal('m1.derivative_mean_usage_system')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import {timeRangeType, shiftTimeRange} from 'shared/query/helpers'
|
||||
import moment from 'moment'
|
||||
import {
|
||||
INVALID,
|
||||
ABSOLUTE,
|
||||
INFLUXQL,
|
||||
RELATIVE_LOWER,
|
||||
RELATIVE_UPPER,
|
||||
} from 'shared/constants/timeRange'
|
||||
const format = INFLUXQL
|
||||
|
||||
describe('Shared.Query.Helpers', () => {
|
||||
describe('timeRangeTypes', () => {
|
||||
it('returns invalid if no upper and lower', () => {
|
||||
const upper = null
|
||||
const lower = null
|
||||
|
||||
const timeRange = {lower, upper}
|
||||
|
||||
expect(timeRangeType(timeRange)).to.equal(INVALID)
|
||||
})
|
||||
|
||||
it('can detect absolute type', () => {
|
||||
const tenMinutes = 600000
|
||||
const upper = Date.now()
|
||||
const lower = upper - tenMinutes
|
||||
|
||||
const timeRange = {lower, upper, format}
|
||||
|
||||
expect(timeRangeType(timeRange)).to.equal(ABSOLUTE)
|
||||
})
|
||||
|
||||
it('can detect exclusive relative lower', () => {
|
||||
const lower = 'now() - 15m'
|
||||
const upper = null
|
||||
|
||||
const timeRange = {lower, upper, format}
|
||||
|
||||
expect(timeRangeType(timeRange)).to.equal(RELATIVE_LOWER)
|
||||
})
|
||||
|
||||
it('can detect relative upper', () => {
|
||||
const upper = 'now()'
|
||||
const oneMinute = 60000
|
||||
const lower = Date.now() - oneMinute
|
||||
|
||||
const timeRange = {lower, upper, format}
|
||||
|
||||
expect(timeRangeType(timeRange)).to.equal(RELATIVE_UPPER)
|
||||
})
|
||||
})
|
||||
|
||||
describe('timeRangeShift', () => {
|
||||
it('can calculate the shift for absolute timeRanges', () => {
|
||||
const upper = Date.now()
|
||||
const oneMinute = 60000
|
||||
const lower = Date.now() - oneMinute
|
||||
const shift = {quantity: 7, unit: 'd'}
|
||||
const timeRange = {upper, lower}
|
||||
|
||||
const type = timeRangeType(timeRange)
|
||||
const actual = shiftTimeRange(timeRange, shift)
|
||||
const expected = {
|
||||
lower: `${lower} - 7d`,
|
||||
upper: `${upper} - 7d`,
|
||||
type: 'shifted',
|
||||
}
|
||||
|
||||
expect(type).to.equal(ABSOLUTE)
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can calculate the shift for relative lower timeRanges', () => {
|
||||
const shift = {quantity: 7, unit: 'd'}
|
||||
const lower = 'now() - 15m'
|
||||
const timeRange = {lower, upper: null}
|
||||
|
||||
const type = timeRangeType(timeRange)
|
||||
const actual = shiftTimeRange(timeRange, shift)
|
||||
const expected = {
|
||||
lower: `${lower} - 7d`,
|
||||
upper: `now() - 7d`,
|
||||
type: 'shifted',
|
||||
}
|
||||
|
||||
expect(type).to.equal(RELATIVE_LOWER)
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can calculate the shift for relative upper timeRanges', () => {
|
||||
const upper = Date.now()
|
||||
const oneMinute = 60000
|
||||
const lower = Date.now() - oneMinute
|
||||
const shift = {quantity: 7, unit: 'd'}
|
||||
const timeRange = {upper, lower}
|
||||
|
||||
const type = timeRangeType(timeRange)
|
||||
const actual = shiftTimeRange(timeRange, shift)
|
||||
const expected = {
|
||||
lower: `${lower} - 7d`,
|
||||
upper: `${upper} - 7d`,
|
||||
type: 'shifted',
|
||||
}
|
||||
|
||||
expect(type).to.equal(ABSOLUTE)
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -228,11 +228,7 @@ describe('timeSeriesToDygraph', () => {
|
|||
]
|
||||
|
||||
const isInDataExplorer = true
|
||||
const actual = timeSeriesToDygraph(
|
||||
influxResponse,
|
||||
undefined,
|
||||
isInDataExplorer
|
||||
)
|
||||
const actual = timeSeriesToDygraph(influxResponse, isInDataExplorer)
|
||||
|
||||
const expected = {}
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@ import React, {PropTypes} from 'react'
|
|||
import OptIn from 'shared/components/OptIn'
|
||||
import Input from 'src/dashboards/components/DisplayOptionsInput'
|
||||
import {Tabber, Tab} from 'src/dashboards/components/Tabber'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants'
|
||||
import {GRAPH_TYPES} from 'src/dashboards/graphics/graph'
|
||||
|
||||
const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS
|
||||
const getInputMin = scale => (scale === LOG ? '0' : null)
|
||||
|
@ -16,86 +19,98 @@ const AxesOptions = ({
|
|||
onSetPrefixSuffix,
|
||||
onSetYAxisBoundMin,
|
||||
onSetYAxisBoundMax,
|
||||
selectedGraphType,
|
||||
}) => {
|
||||
const [min, max] = bounds
|
||||
|
||||
const {menuOption} = GRAPH_TYPES.find(
|
||||
graph => graph.type === selectedGraphType
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="display-options--cell y-axis-controls">
|
||||
<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>
|
||||
<OptIn
|
||||
customPlaceholder={defaultYLabel}
|
||||
customValue={label}
|
||||
onSetValue={onSetLabel}
|
||||
type="text"
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">
|
||||
{menuOption} Controls
|
||||
</h5>
|
||||
<form autoComplete="off" style={{margin: '0 -6px'}}>
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="prefix">Title</label>
|
||||
<OptIn
|
||||
customPlaceholder={defaultYLabel}
|
||||
customValue={label}
|
||||
onSetValue={onSetLabel}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="min">Min</label>
|
||||
<OptIn
|
||||
customPlaceholder={'min'}
|
||||
customValue={min}
|
||||
onSetValue={onSetYAxisBoundMin}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="max">Max</label>
|
||||
<OptIn
|
||||
customPlaceholder={'max'}
|
||||
customValue={max}
|
||||
onSetValue={onSetYAxisBoundMax}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="min">Min</label>
|
||||
<OptIn
|
||||
customPlaceholder={'min'}
|
||||
customValue={min}
|
||||
onSetValue={onSetYAxisBoundMin}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
<Input
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="max">Max</label>
|
||||
<OptIn
|
||||
customPlaceholder={'max'}
|
||||
customValue={max}
|
||||
onSetValue={onSetYAxisBoundMax}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Input
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
tipID="Y-Values's Format"
|
||||
tipContent={TOOLTIP_CONTENT.FORMAT}
|
||||
>
|
||||
<Tab
|
||||
text="K/M/B"
|
||||
isActive={base === BASE_10}
|
||||
onClickTab={onSetBase(BASE_10)}
|
||||
/>
|
||||
<Tab
|
||||
text="K/M/G"
|
||||
isActive={base === BASE_2}
|
||||
onClickTab={onSetBase(BASE_2)}
|
||||
/>
|
||||
</Tabber>
|
||||
<Tabber labelText="Scale">
|
||||
<Tab
|
||||
text="Linear"
|
||||
isActive={scale === LINEAR}
|
||||
onClickTab={onSetScale(LINEAR)}
|
||||
/>
|
||||
<Tab
|
||||
text="Logarithmic"
|
||||
isActive={scale === LOG}
|
||||
onClickTab={onSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
tipID="Y-Values's Format"
|
||||
tipContent={TOOLTIP_CONTENT.FORMAT}
|
||||
>
|
||||
<Tab
|
||||
text="K/M/B"
|
||||
isActive={base === BASE_10}
|
||||
onClickTab={onSetBase(BASE_10)}
|
||||
/>
|
||||
<Tab
|
||||
text="K/M/G"
|
||||
isActive={base === BASE_2}
|
||||
onClickTab={onSetBase(BASE_2)}
|
||||
/>
|
||||
</Tabber>
|
||||
<Tabber labelText="Scale">
|
||||
<Tab
|
||||
text="Linear"
|
||||
isActive={scale === LINEAR}
|
||||
onClickTab={onSetScale(LINEAR)}
|
||||
/>
|
||||
<Tab
|
||||
text="Logarithmic"
|
||||
isActive={scale === LOG}
|
||||
onClickTab={onSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -115,6 +130,7 @@ AxesOptions.defaultProps = {
|
|||
}
|
||||
|
||||
AxesOptions.propTypes = {
|
||||
selectedGraphType: string.isRequired,
|
||||
onSetPrefixSuffix: func.isRequired,
|
||||
onSetYAxisBoundMin: func.isRequired,
|
||||
onSetYAxisBoundMax: func.isRequired,
|
||||
|
|
|
@ -22,12 +22,21 @@ import {
|
|||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
|
||||
import {AUTO_GROUP_BY} from 'shared/constants'
|
||||
import {
|
||||
COLOR_TYPE_THRESHOLD,
|
||||
MAX_THRESHOLDS,
|
||||
DEFAULT_COLORS,
|
||||
GAUGE_COLORS,
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
validateColors,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
class CellEditorOverlay extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const {cell: {name, type, queries, axes}, sources} = props
|
||||
const {cell: {name, type, queries, axes, colors}, sources} = props
|
||||
|
||||
let source = _.get(queries, ['0', 'source'], null)
|
||||
source = sources.find(s => s.links.self === source) || props.source
|
||||
|
@ -47,6 +56,7 @@ class CellEditorOverlay extends Component {
|
|||
activeQueryIndex: 0,
|
||||
isDisplayOptionsTabActive: false,
|
||||
axes,
|
||||
colors: validateColors(colors) ? colors : DEFAULT_COLORS,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,6 +73,111 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleAddThreshold = () => {
|
||||
const {colors} = this.state
|
||||
|
||||
if (colors.length <= MAX_THRESHOLDS) {
|
||||
const randomColor = _.random(0, GAUGE_COLORS.length)
|
||||
|
||||
const maxValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
||||
)
|
||||
const minValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
||||
)
|
||||
|
||||
const colorsValues = _.mapValues(colors, 'value')
|
||||
let randomValue
|
||||
|
||||
do {
|
||||
randomValue = `${_.round(_.random(minValue, maxValue, true), 2)}`
|
||||
} while (_.includes(colorsValues, randomValue))
|
||||
|
||||
const newThreshold = {
|
||||
type: COLOR_TYPE_THRESHOLD,
|
||||
id: uuid.v4(),
|
||||
value: randomValue,
|
||||
hex: GAUGE_COLORS[randomColor].hex,
|
||||
name: GAUGE_COLORS[randomColor].name,
|
||||
}
|
||||
|
||||
this.setState({colors: [...colors, newThreshold]})
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteThreshold = threshold => () => {
|
||||
const {colors} = this.state
|
||||
|
||||
const newColors = colors.filter(color => color.id !== threshold.id)
|
||||
|
||||
this.setState({colors: newColors})
|
||||
}
|
||||
|
||||
handleChooseColor = threshold => chosenColor => {
|
||||
const {colors} = this.state
|
||||
|
||||
const newColors = colors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
: color
|
||||
)
|
||||
|
||||
this.setState({colors: newColors})
|
||||
}
|
||||
|
||||
handleUpdateColorValue = (threshold, newValue) => {
|
||||
const {colors} = this.state
|
||||
const newColors = colors.map(
|
||||
color => (color.id === threshold.id ? {...color, value: newValue} : color)
|
||||
)
|
||||
this.setState({colors: newColors})
|
||||
}
|
||||
|
||||
handleValidateColorValue = (threshold, e) => {
|
||||
const {colors} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const targetValueNumber = Number(e.target.value)
|
||||
|
||||
const maxValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
||||
)
|
||||
const minValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
||||
)
|
||||
|
||||
let allowedToUpdate = false
|
||||
|
||||
// If type === min, make sure it is less than the next threshold
|
||||
if (threshold.type === COLOR_TYPE_MIN) {
|
||||
const nextValue = Number(sortedColors[1].value)
|
||||
allowedToUpdate = targetValueNumber < nextValue && targetValueNumber >= 0
|
||||
}
|
||||
// If type === max, make sure it is greater than the previous threshold
|
||||
if (threshold.type === COLOR_TYPE_MAX) {
|
||||
const previousValue = Number(sortedColors[sortedColors.length - 2].value)
|
||||
allowedToUpdate = previousValue < targetValueNumber
|
||||
}
|
||||
// If type === threshold, make sure new value is greater than min, less than max, and unique
|
||||
if (threshold.type === COLOR_TYPE_THRESHOLD) {
|
||||
const greaterThanMin = targetValueNumber > minValue
|
||||
const lessThanMax = targetValueNumber < maxValue
|
||||
|
||||
const colorsWithoutMinOrMax = sortedColors.slice(
|
||||
1,
|
||||
sortedColors.length - 1
|
||||
)
|
||||
|
||||
const isUnique = !colorsWithoutMinOrMax.some(
|
||||
color => color.value === e.target.value
|
||||
)
|
||||
|
||||
allowedToUpdate = greaterThanMin && lessThanMax && isUnique
|
||||
}
|
||||
|
||||
return allowedToUpdate
|
||||
}
|
||||
|
||||
queryStateReducer = queryModifier => (queryID, ...payload) => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
const query = queriesWorkingDraft.find(q => q.id === queryID)
|
||||
|
@ -145,6 +260,7 @@ class CellEditorOverlay extends Component {
|
|||
cellWorkingType: type,
|
||||
cellWorkingName: name,
|
||||
axes,
|
||||
colors,
|
||||
} = this.state
|
||||
|
||||
const {cell} = this.props
|
||||
|
@ -166,6 +282,7 @@ class CellEditorOverlay extends Component {
|
|||
type,
|
||||
queries,
|
||||
axes,
|
||||
colors,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -296,6 +413,7 @@ class CellEditorOverlay extends Component {
|
|||
|
||||
const {
|
||||
axes,
|
||||
colors,
|
||||
activeQueryIndex,
|
||||
cellWorkingName,
|
||||
cellWorkingType,
|
||||
|
@ -323,6 +441,7 @@ class CellEditorOverlay extends Component {
|
|||
>
|
||||
<Visualization
|
||||
axes={axes}
|
||||
colors={colors}
|
||||
type={cellWorkingType}
|
||||
name={cellWorkingName}
|
||||
timeRange={timeRange}
|
||||
|
@ -347,6 +466,12 @@ class CellEditorOverlay extends Component {
|
|||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions
|
||||
axes={axes}
|
||||
colors={colors}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onAddThreshold={this.handleAddThreshold}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
onSetBase={this.handleSetBase}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
onSetScale={this.handleSetScale}
|
||||
|
|
|
@ -1,54 +1,81 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
|
||||
import SearchBar from 'src/hosts/components/SearchBar'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
const DashboardsPageContents = ({
|
||||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
dashboardLink,
|
||||
}) => {
|
||||
let tableHeader
|
||||
if (dashboards === null) {
|
||||
tableHeader = 'Loading Dashboards...'
|
||||
} else if (dashboards.length === 1) {
|
||||
tableHeader = '1 Dashboard'
|
||||
} else {
|
||||
tableHeader = `${dashboards.length} Dashboards`
|
||||
class DashboardsPageContents extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<DashboardsTable
|
||||
dashboards={dashboards}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onCreateDashboard={onCreateDashboard}
|
||||
dashboardLink={dashboardLink}
|
||||
/>
|
||||
filterDashboards = searchTerm => {
|
||||
this.setState({searchTerm})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
dashboardLink,
|
||||
} = this.props
|
||||
|
||||
let tableHeader
|
||||
if (dashboards === null) {
|
||||
tableHeader = 'Loading Dashboards...'
|
||||
} else if (dashboards.length === 1) {
|
||||
tableHeader = '1 Dashboard'
|
||||
} else {
|
||||
tableHeader = `${dashboards.length} Dashboards`
|
||||
}
|
||||
|
||||
const filteredDashboards = dashboards.filter(d =>
|
||||
d.name.includes(this.state.searchTerm)
|
||||
)
|
||||
|
||||
return (
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<div className="u-flex u-ai-center dashboards-page--actions">
|
||||
<SearchBar
|
||||
placeholder="Filter by Name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<DashboardsTable
|
||||
dashboards={filteredDashboards}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onCreateDashboard={onCreateDashboard}
|
||||
dashboardLink={dashboardLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
|
||||
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
|
||||
import AxesOptions from 'src/dashboards/components/AxesOptions'
|
||||
|
||||
import {buildDefaultYLabel} from 'shared/presenters'
|
||||
|
@ -33,6 +34,7 @@ class DisplayOptions extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
colors,
|
||||
onSetBase,
|
||||
onSetScale,
|
||||
onSetLabel,
|
||||
|
@ -41,24 +43,41 @@ class DisplayOptions extends Component {
|
|||
onSetPrefixSuffix,
|
||||
onSetYAxisBoundMin,
|
||||
onSetYAxisBoundMax,
|
||||
onAddThreshold,
|
||||
onDeleteThreshold,
|
||||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
} = this.props
|
||||
const {axes} = this.state
|
||||
|
||||
const isGauge = selectedGraphType === 'gauge'
|
||||
|
||||
return (
|
||||
<div className="display-options">
|
||||
<AxesOptions
|
||||
axes={axes}
|
||||
onSetBase={onSetBase}
|
||||
onSetLabel={onSetLabel}
|
||||
onSetScale={onSetScale}
|
||||
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
/>
|
||||
<GraphTypeSelector
|
||||
selectedGraphType={selectedGraphType}
|
||||
onSelectGraphType={onSelectGraphType}
|
||||
/>
|
||||
{isGauge
|
||||
? <GaugeOptions
|
||||
colors={colors}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
: <AxesOptions
|
||||
selectedGraphType={selectedGraphType}
|
||||
axes={axes}
|
||||
onSetBase={onSetBase}
|
||||
onSetLabel={onSetLabel}
|
||||
onSetScale={onSetScale}
|
||||
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -66,6 +85,11 @@ class DisplayOptions extends Component {
|
|||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
DisplayOptions.propTypes = {
|
||||
onAddThreshold: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
selectedGraphType: string.isRequired,
|
||||
onSelectGraphType: func.isRequired,
|
||||
onSetPrefixSuffix: func.isRequired,
|
||||
|
@ -75,6 +99,15 @@ DisplayOptions.propTypes = {
|
|||
onSetLabel: func.isRequired,
|
||||
onSetBase: func.isRequired,
|
||||
axes: shape({}).isRequired,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
queryConfigs: arrayOf(shape()).isRequired,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const DisplayOptionsInput = ({id, name, value, onChange, labelText}) =>
|
||||
<div className="form-group col-sm-6">
|
||||
const DisplayOptionsInput = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
labelText,
|
||||
colWidth,
|
||||
placeholder,
|
||||
}) =>
|
||||
<div className={`form-group ${colWidth}`}>
|
||||
<label htmlFor={name}>
|
||||
{labelText}
|
||||
</label>
|
||||
|
@ -12,6 +20,7 @@ const DisplayOptionsInput = ({id, name, value, onChange, labelText}) =>
|
|||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -19,6 +28,8 @@ const {func, string} = PropTypes
|
|||
|
||||
DisplayOptionsInput.defaultProps = {
|
||||
value: '',
|
||||
colWidth: 'col-sm-6',
|
||||
placeholder: '',
|
||||
}
|
||||
|
||||
DisplayOptionsInput.propTypes = {
|
||||
|
@ -27,6 +38,8 @@ DisplayOptionsInput.propTypes = {
|
|||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
labelText: string,
|
||||
colWidth: string,
|
||||
placeholder: string,
|
||||
}
|
||||
|
||||
export default DisplayOptionsInput
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import GaugeThreshold from 'src/dashboards/components/GaugeThreshold'
|
||||
|
||||
import {
|
||||
MAX_THRESHOLDS,
|
||||
MIN_THRESHOLDS,
|
||||
DEFAULT_COLORS,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const GaugeOptions = ({
|
||||
colors,
|
||||
onAddThreshold,
|
||||
onDeleteThreshold,
|
||||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
}) => {
|
||||
const disableMaxColor = colors.length > MIN_THRESHOLDS
|
||||
|
||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Gauge Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
{sortedColors.map(color =>
|
||||
<GaugeThreshold
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
disableMaxColor={disableMaxColor}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
GaugeOptions.defaultProps = {
|
||||
colors: DEFAULT_COLORS,
|
||||
}
|
||||
|
||||
GaugeOptions.propTypes = {
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
onAddThreshold: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
}
|
||||
|
||||
export default GaugeOptions
|
|
@ -0,0 +1,116 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ColorDropdown from 'shared/components/ColorDropdown'
|
||||
|
||||
import {
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
GAUGE_COLORS,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
class GaugeThreshold extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
workingValue: this.props.threshold.value,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeWorkingValue = e => {
|
||||
const {threshold, onValidateColorValue, onUpdateColorValue} = this.props
|
||||
|
||||
const valid = onValidateColorValue(threshold, e)
|
||||
|
||||
if (valid) {
|
||||
onUpdateColorValue(threshold, e.target.value)
|
||||
}
|
||||
|
||||
this.setState({valid, workingValue: e.target.value})
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.setState({workingValue: this.props.threshold.value, valid: true})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
threshold,
|
||||
threshold: {type, hex, name},
|
||||
disableMaxColor,
|
||||
onChooseColor,
|
||||
onDeleteThreshold,
|
||||
} = this.props
|
||||
const {workingValue, valid} = this.state
|
||||
const selectedColor = {hex, name}
|
||||
|
||||
const labelClass =
|
||||
type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX
|
||||
? 'gauge-controls--label'
|
||||
: 'gauge-controls--label-editable'
|
||||
|
||||
const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX)
|
||||
|
||||
let label = 'Threshold'
|
||||
if (type === COLOR_TYPE_MIN) {
|
||||
label = 'Minimum'
|
||||
}
|
||||
if (type === COLOR_TYPE_MAX) {
|
||||
label = 'Maximum'
|
||||
}
|
||||
|
||||
const inputClass = valid
|
||||
? 'form-control input-sm gauge-controls--input'
|
||||
: 'form-control input-sm gauge-controls--input form-volcano'
|
||||
|
||||
return (
|
||||
<div className="gauge-controls--section">
|
||||
<div className={labelClass}>
|
||||
{label}
|
||||
</div>
|
||||
{canBeDeleted
|
||||
? <button
|
||||
className="btn btn-default btn-sm btn-square gauge-controls--delete"
|
||||
onClick={onDeleteThreshold(threshold)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
: null}
|
||||
<input
|
||||
value={workingValue}
|
||||
className={inputClass}
|
||||
type="number"
|
||||
onChange={this.handleChangeWorkingValue}
|
||||
onBlur={this.handleBlur}
|
||||
min={0}
|
||||
/>
|
||||
<ColorDropdown
|
||||
colors={GAUGE_COLORS}
|
||||
selected={selectedColor}
|
||||
onChoose={onChooseColor(threshold)}
|
||||
disabled={type === COLOR_TYPE_MAX && disableMaxColor}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
GaugeThreshold.propTypes = {
|
||||
threshold: shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired,
|
||||
disableMaxColor: bool,
|
||||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
}
|
||||
|
||||
export default GaugeThreshold
|
|
@ -1,29 +1,35 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {graphTypes} from 'src/dashboards/graphics/graph'
|
||||
import {GRAPH_TYPES} 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>
|
||||
<FancyScrollbar
|
||||
className="display-options--cell display-options--cellx2"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Visualization Type</h5>
|
||||
<div className="viz-type-selector">
|
||||
{GRAPH_TYPES.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>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
|
||||
const {func, string} = PropTypes
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ const OverlayControls = ({
|
|||
})}
|
||||
onClick={onClickDisplayOptions(true)}
|
||||
>
|
||||
Options
|
||||
Visualization
|
||||
</li>
|
||||
</ul>
|
||||
<div className="overlay-controls--right">
|
||||
|
|
|
@ -8,12 +8,14 @@ const DashVisualization = (
|
|||
axes,
|
||||
type,
|
||||
name,
|
||||
colors,
|
||||
templates,
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
onCellRename,
|
||||
queryConfigs,
|
||||
editQueryStatus,
|
||||
resizerTopHeight,
|
||||
},
|
||||
{source: {links: {proxy}}}
|
||||
) =>
|
||||
|
@ -21,12 +23,14 @@ const DashVisualization = (
|
|||
<VisualizationName defaultName={name} onCellRename={onCellRename} />
|
||||
<div className="graph-container">
|
||||
<RefreshingGraph
|
||||
colors={colors}
|
||||
axes={axes}
|
||||
type={type}
|
||||
queries={buildQueries(proxy, queryConfigs, timeRange)}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,6 +59,16 @@ DashVisualization.propTypes = {
|
|||
}),
|
||||
}),
|
||||
onCellRename: func,
|
||||
resizerTopHeight: number,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
}
|
||||
|
||||
DashVisualization.contextTypes = {
|
||||
|
|
|
@ -1,33 +1,30 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
|
||||
|
||||
const RowButtons = ({
|
||||
onStartEdit,
|
||||
isEditing,
|
||||
onCancelEdit,
|
||||
onDelete,
|
||||
id,
|
||||
selectedType,
|
||||
}) => {
|
||||
const RowButtons = ({onStartEdit, isEditing, onCancelEdit, onDelete, id}) => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="tvm-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-info"
|
||||
className="btn btn-sm btn-info btn-square"
|
||||
type="button"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
Cancel
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button className="btn btn-sm btn-success" type="submit">
|
||||
{selectedType === 'csv' ? 'Save Values' : 'Get Values'}
|
||||
<button className="btn btn-sm btn-success btn-square" type="submit">
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="tvm-actions">
|
||||
<DeleteConfirmButtons onDelete={onDelete(id)} />
|
||||
<DeleteConfirmButtons
|
||||
onDelete={onDelete(id)}
|
||||
icon="remove"
|
||||
square={true}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-edit btn-square"
|
||||
type="button"
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
export const MAX_THRESHOLDS = 5
|
||||
export const MIN_THRESHOLDS = 2
|
||||
|
||||
export const COLOR_TYPE_MIN = 'min'
|
||||
export const DEFAULT_VALUE_MIN = '0'
|
||||
export const COLOR_TYPE_MAX = 'max'
|
||||
export const DEFAULT_VALUE_MAX = '100'
|
||||
export const COLOR_TYPE_THRESHOLD = 'threshold'
|
||||
|
||||
export const GAUGE_COLORS = [
|
||||
{
|
||||
hex: '#BF3D5E',
|
||||
name: 'ruby',
|
||||
},
|
||||
{
|
||||
hex: '#DC4E58',
|
||||
name: 'fire',
|
||||
},
|
||||
{
|
||||
hex: '#F95F53',
|
||||
name: 'curacao',
|
||||
},
|
||||
{
|
||||
hex: '#F48D38',
|
||||
name: 'tiger',
|
||||
},
|
||||
{
|
||||
hex: '#FFB94A',
|
||||
name: 'pineapple',
|
||||
},
|
||||
{
|
||||
hex: '#FFD255',
|
||||
name: 'thunder',
|
||||
},
|
||||
{
|
||||
hex: '#7CE490',
|
||||
name: 'honeydew',
|
||||
},
|
||||
{
|
||||
hex: '#4ED8A0',
|
||||
name: 'rainforest',
|
||||
},
|
||||
{
|
||||
hex: '#32B08C',
|
||||
name: 'viridian',
|
||||
},
|
||||
{
|
||||
hex: '#4591ED',
|
||||
name: 'ocean',
|
||||
},
|
||||
{
|
||||
hex: '#22ADF6',
|
||||
name: 'pool',
|
||||
},
|
||||
{
|
||||
hex: '#00C9FF',
|
||||
name: 'laser',
|
||||
},
|
||||
{
|
||||
hex: '#513CC6',
|
||||
name: 'planet',
|
||||
},
|
||||
{
|
||||
hex: '#7A65F2',
|
||||
name: 'star',
|
||||
},
|
||||
{
|
||||
hex: '#9394FF',
|
||||
name: 'comet',
|
||||
},
|
||||
{
|
||||
hex: '#383846',
|
||||
name: 'pepper',
|
||||
},
|
||||
{
|
||||
hex: '#545667',
|
||||
name: 'graphite',
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_COLORS = [
|
||||
{
|
||||
type: COLOR_TYPE_MIN,
|
||||
hex: GAUGE_COLORS[11].hex,
|
||||
id: '0',
|
||||
name: GAUGE_COLORS[11].name,
|
||||
value: DEFAULT_VALUE_MIN,
|
||||
},
|
||||
{
|
||||
type: COLOR_TYPE_MAX,
|
||||
hex: GAUGE_COLORS[14].hex,
|
||||
id: '1',
|
||||
name: GAUGE_COLORS[14].name,
|
||||
value: DEFAULT_VALUE_MAX,
|
||||
},
|
||||
]
|
||||
|
||||
export const validateColors = colors => {
|
||||
if (!colors) {
|
||||
return false
|
||||
}
|
||||
const hasMin = colors.some(color => color.type === COLOR_TYPE_MIN)
|
||||
const hasMax = colors.some(color => color.type === COLOR_TYPE_MAX)
|
||||
|
||||
return hasMin && hasMax
|
||||
}
|
|
@ -79,35 +79,29 @@ export const applyMasks = query => {
|
|||
const maskForWholeTemplates = '😸$1😸'
|
||||
return query.replace(matchWholeTemplates, maskForWholeTemplates)
|
||||
}
|
||||
|
||||
export const insertTempVar = (query, tempVar) => {
|
||||
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
|
||||
}
|
||||
|
||||
export const unMask = query => {
|
||||
return query.replace(/😸/g, ':')
|
||||
}
|
||||
|
||||
export const removeUnselectedTemplateValues = templates => {
|
||||
return templates.map(template => {
|
||||
const selectedValues = template.values.filter(value => value.selected)
|
||||
return {...template, values: selectedValues}
|
||||
})
|
||||
}
|
||||
|
||||
export const DISPLAY_OPTIONS = {
|
||||
LINEAR: 'linear',
|
||||
LOG: 'log',
|
||||
BASE_2: '2',
|
||||
BASE_10: '10',
|
||||
}
|
||||
|
||||
export const TOOLTIP_CONTENT = {
|
||||
FORMAT:
|
||||
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
|
||||
}
|
||||
|
||||
export const TYPE_QUERY_CONFIG = 'queryConfig'
|
||||
export const TYPE_SHIFTED = 'shifted queryConfig'
|
||||
export const TYPE_IFQL = 'ifql'
|
||||
|
||||
export const DASHBOARD_NAME_MAX_LENGTH = 50
|
||||
|
|
|
@ -34,18 +34,18 @@ class DashboardPage extends Component {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
dygraphs: [],
|
||||
isEditMode: false,
|
||||
selectedCell: null,
|
||||
isTemplating: false,
|
||||
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
|
||||
names: [],
|
||||
}
|
||||
}
|
||||
|
||||
dygraphs = []
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
params: {dashboardID, sourceID},
|
||||
params: {dashboardID},
|
||||
dashboardActions: {
|
||||
getDashboardsAsync,
|
||||
updateTempVarValues,
|
||||
|
@ -62,13 +62,6 @@ class DashboardPage extends Component {
|
|||
// Refresh and persists influxql generated template variable values
|
||||
await updateTempVarValues(source, dashboard)
|
||||
await putDashboardByID(dashboardID)
|
||||
|
||||
const names = dashboards.map(d => ({
|
||||
name: d.name,
|
||||
link: `/sources/${sourceID}/dashboards/${d.id}`,
|
||||
}))
|
||||
|
||||
this.setState({names})
|
||||
}
|
||||
|
||||
handleOpenTemplateManager = () => {
|
||||
|
@ -178,16 +171,19 @@ class DashboardPage extends Component {
|
|||
}
|
||||
|
||||
synchronizer = dygraph => {
|
||||
const dygraphs = [...this.state.dygraphs, dygraph].filter(d => d.graphDiv)
|
||||
const dygraphs = [...this.dygraphs, dygraph].filter(d => d.graphDiv)
|
||||
const {dashboards, params: {dashboardID}} = this.props
|
||||
|
||||
const dashboard = dashboards.find(
|
||||
d => d.id === idNormalizer(TYPE_ID, dashboardID)
|
||||
)
|
||||
|
||||
// Get only the graphs that can sync the hover line
|
||||
const graphsToSync = dashboard.cells.filter(c => c.type !== 'single-stat')
|
||||
|
||||
if (
|
||||
dashboard &&
|
||||
dygraphs.length === dashboard.cells.length &&
|
||||
dygraphs.length === graphsToSync.length &&
|
||||
dashboard.cells.length > 1
|
||||
) {
|
||||
Dygraph.synchronize(dygraphs, {
|
||||
|
@ -197,7 +193,7 @@ class DashboardPage extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
this.setState({dygraphs})
|
||||
this.dygraphs = dygraphs
|
||||
}
|
||||
|
||||
handleToggleTempVarControls = () => {
|
||||
|
@ -294,7 +290,11 @@ class DashboardPage extends Component {
|
|||
templatesIncludingDashTime = []
|
||||
}
|
||||
|
||||
const {selectedCell, isEditMode, isTemplating, names} = this.state
|
||||
const {selectedCell, isEditMode, isTemplating} = this.state
|
||||
const names = dashboards.map(d => ({
|
||||
name: d.name,
|
||||
link: `/sources/${sourceID}/dashboards/${d.id}`,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
@ -11,40 +11,20 @@ import {getDashboardsAsync, deleteDashboardAsync} from 'src/dashboards/actions'
|
|||
|
||||
import {NEW_DASHBOARD} from 'src/dashboards/constants'
|
||||
|
||||
const {arrayOf, func, string, shape} = PropTypes
|
||||
|
||||
const DashboardsPage = React.createClass({
|
||||
propTypes: {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string,
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: string.isRequired,
|
||||
}),
|
||||
router: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
handleGetDashboards: func.isRequired,
|
||||
handleDeleteDashboard: func.isRequired,
|
||||
dashboards: arrayOf(shape()),
|
||||
},
|
||||
|
||||
class DashboardsPage extends Component {
|
||||
componentDidMount() {
|
||||
this.props.handleGetDashboards()
|
||||
},
|
||||
}
|
||||
|
||||
async handleCreateDashbord() {
|
||||
handleCreateDashbord = async () => {
|
||||
const {source: {id}, router: {push}} = this.props
|
||||
const {data} = await createDashboard(NEW_DASHBOARD)
|
||||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
},
|
||||
}
|
||||
|
||||
handleDeleteDashboard(dashboard) {
|
||||
handleDeleteDashboard = dashboard => {
|
||||
this.props.handleDeleteDashboard(dashboard)
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dashboards} = this.props
|
||||
|
@ -61,8 +41,28 @@ const DashboardsPage = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, string, shape} = PropTypes
|
||||
|
||||
DashboardsPage.propTypes = {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string,
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: string.isRequired,
|
||||
}),
|
||||
router: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
handleGetDashboards: func.isRequired,
|
||||
handleDeleteDashboard: func.isRequired,
|
||||
dashboards: arrayOf(shape()),
|
||||
}
|
||||
|
||||
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
|
||||
dashboards,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
export const graphTypes = [
|
||||
export const GRAPH_TYPES = [
|
||||
{
|
||||
type: 'line',
|
||||
menuOption: 'Line',
|
||||
menuOption: 'Line Graph',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -13,32 +13,32 @@ export const graphTypes = [
|
|||
id="Line"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 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"
|
||||
points="148,40 111.5,47.2 75,25 38.5,90.8 2,111.8 2,125 148,125 "
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
points="2,111.8 38.5,90.8 75,25 111.5,47.2 148,40 "
|
||||
/>
|
||||
<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"
|
||||
points="148,88.2 111.5,95.5 75,61.7 38.5,49.3 2,90.8 2,125 148,125 "
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-line graphic-line-b"
|
||||
points="2,90.8 38.5,49.3 75,61.7 111.5,95.5 148,88.2 "
|
||||
/>
|
||||
<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"
|
||||
points="148,96 111.5,106.3 75,85.7 38.5,116.5 2,115 2,125 148,125 "
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
points="2,115 38.5,116.5 75,85.7 111.5,106.3 148,96 "
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@ export const graphTypes = [
|
|||
},
|
||||
{
|
||||
type: 'line-stacked',
|
||||
menuOption: 'Stacked',
|
||||
menuOption: 'Stacked Graph',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -56,32 +56,32 @@ export const graphTypes = [
|
|||
id="LineStacked"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 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"
|
||||
points="148,25 111.5,25 75,46 38.5,39.1 2,85.5 2,125 148,125 "
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
points="2,85.5 38.5,39.1 75,46 111.5,25 148,25 "
|
||||
/>
|
||||
<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"
|
||||
points="148,53 111.5,49.9 75,88.5 38.5,71 2,116 2,125 148,125 "
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-b"
|
||||
points="2,116 38.5,71 75,88.5 111.5,49.9 148,53 "
|
||||
/>
|
||||
<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"
|
||||
points="148,86.2 111.5,88.6 75,108.6 38.5,98 2,121.1 2,125 148,125 "
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
points="2,121.1 38.5,98 75,108.6 111.5,88.6 148,86.2 "
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -89,7 +89,7 @@ export const graphTypes = [
|
|||
},
|
||||
{
|
||||
type: 'line-stepplot',
|
||||
menuOption: 'Step-Plot',
|
||||
menuOption: 'Step-Plot Graph',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -99,24 +99,32 @@ export const graphTypes = [
|
|||
id="StepPlot"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 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"
|
||||
points="148,61.9 129.8,61.9 129.8,25 93.2,25 93.2,40.6 56.8,40.6 56.8,25 20.2,25 20.2,67.8 2,67.8 2,125 148,125 "
|
||||
/>
|
||||
<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"
|
||||
points="2,67.8 20.2,67.8 20.2,25 56.8,25 56.8,40.6 93.2,40.6 93.2,25 129.8,25 129.8,61.9 148,61.9 "
|
||||
/>
|
||||
<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"
|
||||
points="148,91.9 129.8,91.9 129.8,70.2 93.2,70.2 93.2,67 56.8,67 56.8,50.1 20.2,50.1 20.2,87 2,87 2,125 148,125 "
|
||||
/>
|
||||
<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"
|
||||
points="2,87 20.2,87 20.2,50.1 56.8,50.1 56.8,67 93.2,67 93.2,70.2 129.8,70.2 129.8,91.9 148,91.9 "
|
||||
/>
|
||||
<polygon
|
||||
className="viz-type-selector--graphic-fill graphic-fill-c"
|
||||
points="148,103.5 129.8,103.5 129.8,118.2 93.2,118.2 93.2,84.5 56.8,84.5 56.8,75 20.2,75 20.2,100.2 2,100.2 2,125 148,125 "
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
points="2,100.2 20.2,100.2 20.2,75 56.8,75 56.8,84.5 93.2,84.5 93.2,118.2 129.8,118.2 129.8,103.5 148,103.5 "
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -124,7 +132,7 @@ export const graphTypes = [
|
|||
},
|
||||
{
|
||||
type: 'single-stat',
|
||||
menuOption: 'SingleStat',
|
||||
menuOption: 'Single Stat',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -134,24 +142,32 @@ export const graphTypes = [
|
|||
id="SingleStat"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 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 "
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M35.6,80.4h4.9v1.1h-4.9v7.8h-1.1v-7.8H20.7v-0.6l13.6-20.1h1.3V80.4z M22.4,80.4h12.1V62.1l-1.6,2.7 L22.4,80.4z"
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M58.6,75.1c-0.7,1.5-1.8,2.7-3.2,3.6c-1.5,0.9-3.1,1.4-4.9,1.4c-1.6,0-3-0.4-4.2-1.3s-2.2-2-2.9-3.5 c-0.7-1.5-1.1-3.1-1.1-4.8c0-1.9,0.4-3.6,1.1-5.1c0.7-1.6,1.7-2.8,3-3.7c1.3-0.9,2.7-1.3,4.3-1.3c2.9,0,5.2,1,6.7,2.9 c1.5,1.9,2.3,4.7,2.3,8.3v3.3c0,4.8-1.1,8.5-3.2,11c-2.1,2.5-5.3,3.8-9.4,3.9H46l0-1.1h0.8c3.8,0,6.7-1.2,8.7-3.5 C57.6,82.8,58.6,79.5,58.6,75.1z M50.4,79c1.9,0,3.6-0.6,5.1-1.7s2.5-2.6,3-4.5v-1.2c0-3.3-0.7-5.8-2-7.5c-1.4-1.7-3.3-2.6-5.8-2.6 c-1.4,0-2.7,0.4-3.8,1.2s-2,1.9-2.6,3.3c-0.6,1.4-0.9,2.9-0.9,4.5c0,1.5,0.3,3,0.9,4.3c0.6,1.3,1.5,2.4,2.5,3.1 C47.8,78.7,49.1,79,50.4,79z"
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M81.3,89.2h-17v-1.1L74,77c1.6-1.9,2.8-3.5,3.5-5c0.8-1.4,1.2-2.8,1.2-4c0-2.1-0.6-3.7-1.8-4.9 c-1.2-1.2-2.9-1.7-5.1-1.7c-1.3,0-2.5,0.3-3.6,1c-1.1,0.6-2,1.5-2.6,2.6c-0.6,1.1-0.9,2.4-0.9,3.8h-1.1c0-1.5,0.4-2.9,1.1-4.2 c0.7-1.3,1.7-2.3,2.9-3.1s2.6-1.1,4.2-1.1c2.5,0,4.5,0.7,5.9,2c1.4,1.3,2.1,3.2,2.1,5.6c0,2.2-1.2,4.9-3.7,7.9l-1.8,2.2l-8.6,10 h15.6V89.2z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M85.3,88.3c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3c0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8 c0,0.3-0.1,0.6-0.3,0.8s-0.5,0.3-0.8,0.3c-0.3,0-0.6-0.1-0.8-0.3C85.4,88.8,85.3,88.6,85.3,88.3z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M92.7,74.3L94,60.8h13.9v1.1H95l-1.2,11.4c0.7-0.6,1.6-1,2.7-1.4s2.2-0.5,3.3-0.5c2.6,0,4.6,0.8,6.1,2.4 c1.5,1.6,2.3,3.8,2.3,6.4c0,3.1-0.7,5.4-2.1,7c-1.4,1.6-3.4,2.4-5.9,2.4c-2.4,0-4.4-0.7-5.9-2.1c-1.5-1.4-2.3-3.3-2.5-5.8h1.1 c0.2,2.2,0.9,3.9,2.2,5.1c1.2,1.2,3,1.7,5.2,1.7c2.3,0,4.1-0.7,5.2-2.1c1.1-1.4,1.7-3.5,1.7-6.2c0-2.4-0.7-4.3-2-5.7 c-1.3-1.4-3.1-2.1-5.3-2.1c-1.4,0-2.6,0.2-3.6,0.5c-1,0.4-1.9,0.9-2.7,1.7L92.7,74.3z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M113.8,74.3l1.3-13.6H129v1.1h-12.9l-1.2,11.4c0.7-0.6,1.6-1,2.7-1.4s2.2-0.5,3.3-0.5c2.6,0,4.6,0.8,6.1,2.4 c1.5,1.6,2.3,3.8,2.3,6.4c0,3.1-0.7,5.4-2.1,7c-1.4,1.6-3.4,2.4-5.9,2.4c-2.4,0-4.4-0.7-5.9-2.1c-1.5-1.4-2.3-3.3-2.5-5.8h1.1 c0.2,2.2,0.9,3.9,2.2,5.1c1.2,1.2,3,1.7,5.2,1.7c2.3,0,4.1-0.7,5.2-2.1c1.1-1.4,1.7-3.5,1.7-6.2c0-2.4-0.7-4.3-2-5.7 c-1.3-1.4-3.1-2.1-5.3-2.1c-1.4,0-2.6,0.2-3.6,0.5c-1,0.4-1.9,0.9-2.7,1.7L113.8,74.3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -159,7 +175,7 @@ export const graphTypes = [
|
|||
},
|
||||
{
|
||||
type: 'line-plus-single-stat',
|
||||
menuOption: 'Line + Stat',
|
||||
menuOption: 'Line Graph + Single Stat',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -169,40 +185,42 @@ export const graphTypes = [
|
|||
id="LineAndSingleStat"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 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"
|
||||
<g>
|
||||
<polygon
|
||||
className="viz-type-selector--graphic-fill graphic-fill-c"
|
||||
points="148,88.2 111.5,95.5 75,25 38.5,54.7 2,66.7 2,125 148,125"
|
||||
/>
|
||||
<polyline
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
points="2,66.7 38.5,54.7 75,25 111.5,95.5 148,88.2"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M35.6,80.4h4.9v1.1h-4.9v7.8h-1.1v-7.8H20.7v-0.6l13.6-20.1h1.3V80.4z M22.4,80.4h12.1V62.1l-1.6,2.7 L22.4,80.4z"
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M58.6,75.1c-0.7,1.5-1.8,2.7-3.2,3.6c-1.5,0.9-3.1,1.4-4.9,1.4c-1.6,0-3-0.4-4.2-1.3s-2.2-2-2.9-3.5 c-0.7-1.5-1.1-3.1-1.1-4.8c0-1.9,0.4-3.6,1.1-5.1c0.7-1.6,1.7-2.8,3-3.7c1.3-0.9,2.7-1.3,4.3-1.3c2.9,0,5.2,1,6.7,2.9 c1.5,1.9,2.3,4.7,2.3,8.3v3.3c0,4.8-1.1,8.5-3.2,11c-2.1,2.5-5.3,3.8-9.4,3.9H46l0-1.1h0.8c3.8,0,6.7-1.2,8.7-3.5 C57.6,82.8,58.6,79.5,58.6,75.1z M50.4,79c1.9,0,3.6-0.6,5.1-1.7s2.5-2.6,3-4.5v-1.2c0-3.3-0.7-5.8-2-7.5c-1.4-1.7-3.3-2.6-5.8-2.6 c-1.4,0-2.7,0.4-3.8,1.2s-2,1.9-2.6,3.3c-0.6,1.4-0.9,2.9-0.9,4.5c0,1.5,0.3,3,0.9,4.3c0.6,1.3,1.5,2.4,2.5,3.1 C47.8,78.7,49.1,79,50.4,79z"
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M81.3,89.2h-17v-1.1L74,77c1.6-1.9,2.8-3.5,3.5-5c0.8-1.4,1.2-2.8,1.2-4c0-2.1-0.6-3.7-1.8-4.9 c-1.2-1.2-2.9-1.7-5.1-1.7c-1.3,0-2.5,0.3-3.6,1c-1.1,0.6-2,1.5-2.6,2.6c-0.6,1.1-0.9,2.4-0.9,3.8h-1.1c0-1.5,0.4-2.9,1.1-4.2 c0.7-1.3,1.7-2.3,2.9-3.1s2.6-1.1,4.2-1.1c2.5,0,4.5,0.7,5.9,2c1.4,1.3,2.1,3.2,2.1,5.6c0,2.2-1.2,4.9-3.7,7.9l-1.8,2.2l-8.6,10 h15.6V89.2z"
|
||||
/>
|
||||
<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"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M85.3,88.3c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3c0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8 c0,0.3-0.1,0.6-0.3,0.8s-0.5,0.3-0.8,0.3c-0.3,0-0.6-0.1-0.8-0.3C85.4,88.8,85.3,88.6,85.3,88.3z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M92.7,74.3L94,60.8h13.9v1.1H95l-1.2,11.4c0.7-0.6,1.6-1,2.7-1.4s2.2-0.5,3.3-0.5c2.6,0,4.6,0.8,6.1,2.4 c1.5,1.6,2.3,3.8,2.3,6.4c0,3.1-0.7,5.4-2.1,7c-1.4,1.6-3.4,2.4-5.9,2.4c-2.4,0-4.4-0.7-5.9-2.1c-1.5-1.4-2.3-3.3-2.5-5.8h1.1 c0.2,2.2,0.9,3.9,2.2,5.1c1.2,1.2,3,1.7,5.2,1.7c2.3,0,4.1-0.7,5.2-2.1c1.1-1.4,1.7-3.5,1.7-6.2c0-2.4-0.7-4.3-2-5.7 c-1.3-1.4-3.1-2.1-5.3-2.1c-1.4,0-2.6,0.2-3.6,0.5c-1,0.4-1.9,0.9-2.7,1.7L92.7,74.3z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M113.8,74.3l1.3-13.6H129v1.1h-12.9l-1.2,11.4c0.7-0.6,1.6-1,2.7-1.4s2.2-0.5,3.3-0.5c2.6,0,4.6,0.8,6.1,2.4 c1.5,1.6,2.3,3.8,2.3,6.4c0,3.1-0.7,5.4-2.1,7c-1.4,1.6-3.4,2.4-5.9,2.4c-2.4,0-4.4-0.7-5.9-2.1c-1.5-1.4-2.3-3.3-2.5-5.8h1.1 c0.2,2.2,0.9,3.9,2.2,5.1c1.2,1.2,3,1.7,5.2,1.7c2.3,0,4.1-0.7,5.2-2.1c1.1-1.4,1.7-3.5,1.7-6.2c0-2.4-0.7-4.3-2-5.7 c-1.3-1.4-3.1-2.1-5.3-2.1c-1.4,0-2.6,0.2-3.6,0.5c-1,0.4-1.9,0.9-2.7,1.7L113.8,74.3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -210,7 +228,7 @@ export const graphTypes = [
|
|||
},
|
||||
{
|
||||
type: 'bar',
|
||||
menuOption: 'Bar',
|
||||
menuOption: 'Bar Graph',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
|
@ -220,56 +238,222 @@ export const graphTypes = [
|
|||
id="Bar"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 150"
|
||||
viewBox="0 0 150 150"
|
||||
preserveAspectRatio="none meet"
|
||||
>
|
||||
<path
|
||||
<rect
|
||||
x="2"
|
||||
y="108.4"
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
width="26.8"
|
||||
height="16.6"
|
||||
/>
|
||||
<rect
|
||||
x="31.8"
|
||||
y="82.4"
|
||||
className="viz-type-selector--graphic-line graphic-line-b"
|
||||
width="26.8"
|
||||
height="42.6"
|
||||
/>
|
||||
<rect
|
||||
x="61.6"
|
||||
y="28.8"
|
||||
className="viz-type-selector--graphic-line graphic-line-c"
|
||||
width="26.8"
|
||||
height="96.2"
|
||||
/>
|
||||
<rect
|
||||
x="91.4"
|
||||
y="47.9"
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
width="26.8"
|
||||
height="77.1"
|
||||
/>
|
||||
<rect
|
||||
x="121.2"
|
||||
y="25"
|
||||
className="viz-type-selector--graphic-line graphic-line-b"
|
||||
width="26.8"
|
||||
height="100"
|
||||
/>
|
||||
<rect
|
||||
x="2"
|
||||
y="108.4"
|
||||
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"
|
||||
width="26.8"
|
||||
height="16.6"
|
||||
/>
|
||||
<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
|
||||
<rect
|
||||
x="31.8"
|
||||
y="82.4"
|
||||
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"
|
||||
width="26.8"
|
||||
height="42.6"
|
||||
/>
|
||||
<rect
|
||||
x="61.6"
|
||||
y="28.8"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-c"
|
||||
width="26.8"
|
||||
height="96.2"
|
||||
/>
|
||||
<rect
|
||||
x="91.4"
|
||||
y="47.9"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-a"
|
||||
width="26.8"
|
||||
height="77.1"
|
||||
/>
|
||||
<rect
|
||||
x="121.2"
|
||||
y="25"
|
||||
className="viz-type-selector--graphic-fill graphic-fill-b"
|
||||
width="26.8"
|
||||
height="100"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'gauge',
|
||||
menuOption: 'Gauge',
|
||||
graphic: (
|
||||
<div className="viz-type-selector--graphic">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
version="1.1"
|
||||
id="Bar"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 150 150"
|
||||
preserveAspectRatio="none meet"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
d="M110.9,110.9c19.9-19.9,19.9-52,0-71.9s-52-19.9-71.9,0s-19.9,52,0,71.9"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="39.1"
|
||||
y1="110.9"
|
||||
x2="35"
|
||||
y2="115"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="110.9"
|
||||
y1="110.9"
|
||||
x2="115"
|
||||
y2="115"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="122"
|
||||
y1="94.5"
|
||||
x2="127.2"
|
||||
y2="96.6"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="125.8"
|
||||
y1="75"
|
||||
x2="131.5"
|
||||
y2="75"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="122"
|
||||
y1="55.5"
|
||||
x2="127.2"
|
||||
y2="53.4"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="110.9"
|
||||
y1="39.1"
|
||||
x2="115"
|
||||
y2="35"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="94.5"
|
||||
y1="28"
|
||||
x2="96.6"
|
||||
y2="22.8"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="75"
|
||||
y1="24.2"
|
||||
x2="75"
|
||||
y2="18.5"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="55.5"
|
||||
y1="28"
|
||||
x2="53.4"
|
||||
y2="22.8"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="39.1"
|
||||
y1="39.1"
|
||||
x2="35"
|
||||
y2="35"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="28"
|
||||
y1="55.5"
|
||||
x2="22.8"
|
||||
y2="53.4"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="24.2"
|
||||
y1="75"
|
||||
x2="18.5"
|
||||
y2="75"
|
||||
/>
|
||||
<line
|
||||
className="viz-type-selector--graphic-line graphic-line-d"
|
||||
x1="28"
|
||||
y1="94.5"
|
||||
x2="22.8"
|
||||
y2="96.6"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
className="viz-type-selector--graphic-fill graphic-fill-d"
|
||||
d="M78.6,73.4L75,56.3l-3.6,17.1c-0.2,0.5-0.3,1-0.3,1.6c0,2.2,1.8,3.9,3.9,3.9s3.9-1.8,3.9-3.9C78.9,74.4,78.8,73.9,78.6,73.4z"
|
||||
/>
|
||||
<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"
|
||||
d="M58.9,58.9c8.9-8.9,23.4-8.9,32.3,0l17.1-17.1c-18.4-18.4-48.2-18.4-66.5,0C32.5,50.9,27.9,63,27.9,75h24.2C52.2,69.2,54.4,63.3,58.9,58.9z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-line graphic-line-a"
|
||||
d="M58.9,58.9c8.9-8.9,23.4-8.9,32.3,0l17.1-17.1c-18.4-18.4-48.2-18.4-66.5,0C32.5,50.9,27.9,63,27.9,75h24.2C52.2,69.2,54.4,63.3,58.9,58.9z"
|
||||
/>
|
||||
<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"
|
||||
d="M58.9,91.1c-4.5-4.5-6.7-10.3-6.7-16.1H27.9c0,12,4.6,24.1,13.8,33.3L58.9,91.1z"
|
||||
/>
|
||||
<path
|
||||
className="viz-type-selector--graphic-line graphic-line-b"
|
||||
d="M58.9,91.1c-4.5-4.5-6.7-10.3-6.7-16.1H27.9c0,12,4.6,24.1,13.8,33.3L58.9,91.1z"
|
||||
/>
|
||||
<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"
|
||||
d="M91.1,91.1l17.1,17.1c18.4-18.4,18.4-48.2,0-66.6L91.1,58.9C100.1,67.8,100.1,82.2,91.1,91.1z"
|
||||
/>
|
||||
<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"
|
||||
d="M91.1,91.1l17.1,17.1c18.4-18.4,18.4-48.2,0-66.6L91.1,58.9C100.1,67.8,100.1,82.2,91.1,91.1z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
@ -18,26 +18,26 @@ export const deleteQuery = queryID => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryId, fieldFunc) => ({
|
||||
export const toggleField = (queryID, fieldFunc) => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryId, time) => ({
|
||||
export const groupByTime = (queryID, time) => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
export const fill = (queryId, value) => ({
|
||||
export const fill = (queryID, value) => ({
|
||||
type: 'DE_FILL',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
@ -51,44 +51,44 @@ export const removeFuncs = (queryID, fields, groupBy) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryId, fieldFunc, groupBy) => ({
|
||||
export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
fieldFunc,
|
||||
groupBy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryId, tag) => ({
|
||||
export const chooseTag = (queryID, tag) => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({
|
||||
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryId, measurement) => ({
|
||||
export const chooseMeasurement = (queryID, measurement) => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRawText = (queryId, rawText) => ({
|
||||
export const editRawText = (queryID, rawText) => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
@ -100,18 +100,18 @@ export const setTimeRange = bounds => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryId, tagKey) => ({
|
||||
export const groupByTag = (queryID, tagKey) => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryId => ({
|
||||
export const toggleTagAcceptance = queryID => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -147,6 +147,14 @@ export const editQueryStatus = (queryID, status) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const timeShift = (queryID, shift) => ({
|
||||
type: 'DE_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (url, id, text) => async dispatch => {
|
||||
try {
|
||||
|
|
|
@ -7,13 +7,10 @@ import Dropdown from 'shared/components/Dropdown'
|
|||
|
||||
import {AUTO_GROUP_BY} from 'shared/constants'
|
||||
|
||||
const {func, string, shape} = PropTypes
|
||||
|
||||
const isInRuleBuilder = pathname => pathname.includes('alert-rules')
|
||||
const isInDataExplorer = pathname => pathname.includes('data-explorer')
|
||||
|
||||
const getOptions = pathname =>
|
||||
isInDataExplorer(pathname) || isInRuleBuilder(pathname)
|
||||
isInRuleBuilder(pathname)
|
||||
? groupByTimeOptions.filter(({menuOption}) => menuOption !== AUTO_GROUP_BY)
|
||||
: groupByTimeOptions
|
||||
|
||||
|
@ -37,6 +34,8 @@ const GroupByTimeDropdown = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
const {func, string, shape} = PropTypes
|
||||
|
||||
GroupByTimeDropdown.propTypes = {
|
||||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
|
|
|
@ -7,6 +7,7 @@ import {Table, Column, Cell} from 'fixed-data-table'
|
|||
import Dropdown from 'shared/components/Dropdown'
|
||||
import CustomCell from 'src/data_explorer/components/CustomCell'
|
||||
import TabItem from 'src/data_explorer/components/TableTabItem'
|
||||
import {TEMPLATES} from 'src/data_explorer/constants'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
|
||||
|
@ -43,7 +44,11 @@ class ChronoTable extends Component {
|
|||
this.setState({isLoading: true})
|
||||
// second param is db, we want to leave this blank
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({source: query.host, query})
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
this.setState({isLoading: false})
|
||||
|
||||
let series = _.get(results, ['0', 'series'], [])
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import classnames from 'classnames'
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
import {GRAPH, TABLE} from 'shared/constants'
|
||||
import buildQueries from 'utils/buildQueriesForGraphs'
|
||||
import _ from 'lodash'
|
||||
|
||||
const META_QUERY_REGEX = /^show/i
|
||||
const META_QUERY_REGEX = /^(show|create|drop)/i
|
||||
|
||||
class Visualization extends Component {
|
||||
constructor(props) {
|
||||
|
@ -61,19 +61,11 @@ class Visualization extends Component {
|
|||
resizerBottomHeight,
|
||||
errorThrown,
|
||||
} = this.props
|
||||
|
||||
const {source: {links: {proxy}}} = this.context
|
||||
const {view} = this.state
|
||||
|
||||
const statements = queryConfigs.map(query => {
|
||||
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, queryConfig: s.queryConfig}
|
||||
})
|
||||
|
||||
const queries = buildQueries(proxy, queryConfigs, timeRange)
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
const query = activeQuery || defaultQuery
|
||||
|
@ -81,12 +73,12 @@ class Visualization extends Component {
|
|||
return (
|
||||
<div className="graph" style={{height}}>
|
||||
<VisHeader
|
||||
views={views}
|
||||
view={view}
|
||||
onToggleView={this.handleToggleView}
|
||||
name={cellName}
|
||||
views={views}
|
||||
query={query}
|
||||
name={cellName}
|
||||
errorThrown={errorThrown}
|
||||
onToggleView={this.handleToggleView}
|
||||
/>
|
||||
<div
|
||||
className={classnames({
|
||||
|
|
|
@ -81,3 +81,16 @@ export const QUERY_TEMPLATES = [
|
|||
{text: 'Show Stats', query: 'SHOW STATS'},
|
||||
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
|
||||
]
|
||||
|
||||
const interval = {
|
||||
id: 'interval',
|
||||
type: 'autoGroupBy',
|
||||
tempVar: ':interval:',
|
||||
label: 'automatically determine the best group by time',
|
||||
values: [
|
||||
{value: '1000', type: 'resolution', selected: true},
|
||||
{value: '3', type: 'pointsPerPixel', selected: true},
|
||||
],
|
||||
} // pixels
|
||||
|
||||
export const TEMPLATES = [interval]
|
||||
|
|
|
@ -14,8 +14,8 @@ import ResizeContainer from 'shared/components/ResizeContainer'
|
|||
import OverlayTechnologies from 'shared/components/OverlayTechnologies'
|
||||
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
||||
|
||||
import {VIS_VIEWS, INITIAL_GROUP_BY_TIME} from 'shared/constants'
|
||||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from '../constants'
|
||||
import {VIS_VIEWS, AUTO_GROUP_BY} from 'shared/constants'
|
||||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS, TEMPLATES} from '../constants'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
import * as dataExplorerActionCreators from 'src/data_explorer/actions/view'
|
||||
|
@ -88,7 +88,6 @@ class DataExplorer extends Component {
|
|||
|
||||
const {showWriteForm} = this.state
|
||||
const selectedDatabase = _.get(queryConfigs, ['0', 'database'], null)
|
||||
|
||||
return (
|
||||
<div className="data-explorer">
|
||||
{showWriteForm
|
||||
|
@ -122,12 +121,13 @@ class DataExplorer extends Component {
|
|||
actions={queryConfigActions}
|
||||
timeRange={timeRange}
|
||||
activeQuery={this.getActiveQuery()}
|
||||
initialGroupByTime={INITIAL_GROUP_BY_TIME}
|
||||
initialGroupByTime={AUTO_GROUP_BY}
|
||||
/>
|
||||
<Visualization
|
||||
views={VIS_VIEWS}
|
||||
activeQueryIndex={0}
|
||||
timeRange={timeRange}
|
||||
templates={TEMPLATES}
|
||||
autoRefresh={autoRefresh}
|
||||
queryConfigs={queryConfigs}
|
||||
manualRefresh={manualRefresh}
|
||||
|
|
|
@ -3,6 +3,7 @@ import _ from 'lodash'
|
|||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
fill,
|
||||
timeShift,
|
||||
chooseTag,
|
||||
groupByTag,
|
||||
removeFuncs,
|
||||
|
@ -20,24 +21,24 @@ import {
|
|||
const queryConfigs = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case 'DE_CHOOSE_NAMESPACE': {
|
||||
const {queryId, database, retentionPolicy} = action.payload
|
||||
const nextQueryConfig = chooseNamespace(state[queryId], {
|
||||
const {queryID, database, retentionPolicy} = action.payload
|
||||
const nextQueryConfig = chooseNamespace(state[queryID], {
|
||||
database,
|
||||
retentionPolicy,
|
||||
})
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
[queryID]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_MEASUREMENT': {
|
||||
const {queryId, measurement} = action.payload
|
||||
const nextQueryConfig = chooseMeasurement(state[queryId], measurement)
|
||||
const {queryID, measurement} = action.payload
|
||||
const nextQueryConfig = chooseMeasurement(state[queryID], measurement)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: Object.assign(nextQueryConfig, {
|
||||
rawText: state[queryId].rawText,
|
||||
[queryID]: Object.assign(nextQueryConfig, {
|
||||
rawText: state[queryID].rawText,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
@ -64,78 +65,78 @@ const queryConfigs = (state = {}, action) => {
|
|||
}
|
||||
|
||||
case 'DE_EDIT_RAW_TEXT': {
|
||||
const {queryId, rawText} = action.payload
|
||||
const nextQueryConfig = editRawText(state[queryId], rawText)
|
||||
const {queryID, rawText} = action.payload
|
||||
const nextQueryConfig = editRawText(state[queryID], rawText)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TIME': {
|
||||
const {queryId, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryId], time)
|
||||
const {queryID, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryID], time)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryId} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryId])
|
||||
const {queryID} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryID])
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_TOGGLE_FIELD': {
|
||||
const {queryId, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleField(state[queryId], fieldFunc)
|
||||
const {queryID, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleField(state[queryID], fieldFunc)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: {...nextQueryConfig, rawText: null},
|
||||
[queryID]: {...nextQueryConfig, rawText: null},
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_APPLY_FUNCS_TO_FIELD': {
|
||||
const {queryId, fieldFunc, groupBy} = action.payload
|
||||
const {queryID, fieldFunc, groupBy} = action.payload
|
||||
const nextQueryConfig = applyFuncsToField(
|
||||
state[queryId],
|
||||
state[queryID],
|
||||
fieldFunc,
|
||||
groupBy
|
||||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_CHOOSE_TAG': {
|
||||
const {queryId, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryId], tag)
|
||||
const {queryID, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryID], tag)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_GROUP_BY_TAG': {
|
||||
const {queryId, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryId], tagKey)
|
||||
const {queryID, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryID], tagKey)
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'DE_FILL': {
|
||||
const {queryId, value} = action.payload
|
||||
const nextQueryConfig = fill(state[queryId], value)
|
||||
const {queryID, value} = action.payload
|
||||
const nextQueryConfig = fill(state[queryID], value)
|
||||
|
||||
return {
|
||||
...state,
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +172,13 @@ const queryConfigs = (state = {}, action) => {
|
|||
|
||||
return {...state, [queryID]: nextQuery}
|
||||
}
|
||||
|
||||
case 'DE_TIME_SHIFT': {
|
||||
const {queryID, shift} = action.payload
|
||||
const nextQuery = timeShift(state[queryID], shift)
|
||||
|
||||
return {...state, [queryID]: nextQuery}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -196,17 +196,21 @@ function parseSeries(series) {
|
|||
function parseTag(s, obj) {
|
||||
const match = tag.exec(s)
|
||||
|
||||
const kv = match[0]
|
||||
const key = match[1]
|
||||
const value = match[2]
|
||||
if (match) {
|
||||
const kv = match[0]
|
||||
const key = match[1]
|
||||
const value = match[2]
|
||||
|
||||
if (key) {
|
||||
if (!obj.tags) {
|
||||
obj.tags = {}
|
||||
if (key) {
|
||||
if (!obj.tags) {
|
||||
obj.tags = {}
|
||||
}
|
||||
obj.tags[key] = value
|
||||
}
|
||||
obj.tags[key] = value
|
||||
return s.slice(match.index + kv.length)
|
||||
}
|
||||
return s.slice(match.index + kv.length)
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
let workStr = series.slice()
|
||||
|
|
|
@ -103,7 +103,10 @@ class HostsTable extends Component {
|
|||
<h2 className="panel-title">
|
||||
{hostsTitle}
|
||||
</h2>
|
||||
<SearchBar onSearch={this.updateSearchTerm} />
|
||||
<SearchBar
|
||||
placeholder="Filter by Host..."
|
||||
onSearch={this.updateSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{hostCount > 0 && !hostsError.length
|
||||
|
|
|
@ -10,8 +10,7 @@ class SearchBar extends Component {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
const waitPeriod = 300
|
||||
this.handleSearch = _.debounce(this.handleSearch, waitPeriod)
|
||||
this.handleSearch = _.debounce(this.handleSearch, 50)
|
||||
}
|
||||
|
||||
handleSearch = () => {
|
||||
|
@ -23,12 +22,13 @@ class SearchBar extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {placeholder} = this.props
|
||||
return (
|
||||
<div className="users__search-widget input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Filter by Host..."
|
||||
placeholder={placeholder}
|
||||
ref="searchInput"
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
@ -40,10 +40,11 @@ class SearchBar extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {func} = PropTypes
|
||||
const {func, string} = PropTypes
|
||||
|
||||
SearchBar.propTypes = {
|
||||
onSearch: func.isRequired,
|
||||
placeholder: string.isRequired,
|
||||
}
|
||||
|
||||
export default SearchBar
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import HostsTable from 'src/hosts/components/HostsTable'
|
||||
|
@ -7,27 +7,16 @@ import SourceIndicator from 'shared/components/SourceIndicator'
|
|||
|
||||
import {getCpuAndLoadForHosts, getMappings, getAppsForHosts} from '../apis'
|
||||
|
||||
export const HostsPage = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string, // 'influx-enterprise'
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: PropTypes.string.isRequired,
|
||||
}),
|
||||
addFlashMessage: PropTypes.func,
|
||||
},
|
||||
class HostsPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
this.state = {
|
||||
hosts: {},
|
||||
hostsLoading: true,
|
||||
hostsError: '',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
|
@ -71,7 +60,7 @@ export const HostsPage = React.createClass({
|
|||
// (like with a bogus proxy link). We should provide better messaging to the user in this catch after that's fixed.
|
||||
console.error(reason) // eslint-disable-line no-console
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source} = this.props
|
||||
|
@ -104,7 +93,22 @@ export const HostsPage = React.createClass({
|
|||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
HostsPage.propTypes = {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
type: string, // 'influx-enterprise'
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: string.isRequired,
|
||||
}),
|
||||
addFlashMessage: func,
|
||||
}
|
||||
|
||||
export default HostsPage
|
||||
|
|
|
@ -1,63 +1,63 @@
|
|||
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({
|
||||
export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
|
||||
type: 'KAPA_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryId, measurement) => ({
|
||||
export const chooseMeasurement = (queryID, measurement) => ({
|
||||
type: 'KAPA_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryId, tag) => ({
|
||||
export const chooseTag = (queryID, tag) => ({
|
||||
type: 'KAPA_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryId, tagKey) => ({
|
||||
export const groupByTag = (queryID, tagKey) => ({
|
||||
type: 'KAPA_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryId => ({
|
||||
export const toggleTagAcceptance = queryID => ({
|
||||
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryId, fieldFunc) => ({
|
||||
export const toggleField = (queryID, fieldFunc) => ({
|
||||
type: 'KAPA_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryId, fieldFunc) => ({
|
||||
export const applyFuncsToField = (queryID, fieldFunc) => ({
|
||||
type: 'KAPA_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryId, time) => ({
|
||||
export const groupByTime = (queryID, time) => ({
|
||||
type: 'KAPA_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryId,
|
||||
queryID,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
@ -69,3 +69,11 @@ export const removeFuncs = (queryID, fields) => ({
|
|||
fields,
|
||||
},
|
||||
})
|
||||
|
||||
export const timeShift = (queryID, shift) => ({
|
||||
type: 'KAPA_TIME_SHIFT',
|
||||
payload: {
|
||||
queryID,
|
||||
shift,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -66,7 +66,7 @@ export const getRule = (kapacitor, ruleID) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export function loadDefaultRule() {
|
||||
export const loadDefaultRule = () => {
|
||||
return dispatch => {
|
||||
const queryID = uuid.v4()
|
||||
dispatch({
|
||||
|
@ -88,15 +88,13 @@ export const fetchRules = kapacitor => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export function chooseTrigger(ruleID, trigger) {
|
||||
return {
|
||||
type: 'CHOOSE_TRIGGER',
|
||||
payload: {
|
||||
ruleID,
|
||||
trigger,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const chooseTrigger = (ruleID, trigger) => ({
|
||||
type: 'CHOOSE_TRIGGER',
|
||||
payload: {
|
||||
ruleID,
|
||||
trigger,
|
||||
},
|
||||
})
|
||||
|
||||
export const addEvery = (ruleID, frequency) => ({
|
||||
type: 'ADD_EVERY',
|
||||
|
@ -113,36 +111,30 @@ export const removeEvery = ruleID => ({
|
|||
},
|
||||
})
|
||||
|
||||
export function updateRuleValues(ruleID, trigger, values) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_VALUES',
|
||||
payload: {
|
||||
ruleID,
|
||||
trigger,
|
||||
values,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const updateRuleValues = (ruleID, trigger, values) => ({
|
||||
type: 'UPDATE_RULE_VALUES',
|
||||
payload: {
|
||||
ruleID,
|
||||
trigger,
|
||||
values,
|
||||
},
|
||||
})
|
||||
|
||||
export function updateMessage(ruleID, message) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_MESSAGE',
|
||||
payload: {
|
||||
ruleID,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const updateMessage = (ruleID, message) => ({
|
||||
type: 'UPDATE_RULE_MESSAGE',
|
||||
payload: {
|
||||
ruleID,
|
||||
message,
|
||||
},
|
||||
})
|
||||
|
||||
export function updateDetails(ruleID, details) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_DETAILS',
|
||||
payload: {
|
||||
ruleID,
|
||||
details,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const updateDetails = (ruleID, details) => ({
|
||||
type: 'UPDATE_RULE_DETAILS',
|
||||
payload: {
|
||||
ruleID,
|
||||
details,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
|
||||
type: 'UPDATE_RULE_ALERT_PROPERTY',
|
||||
|
@ -160,66 +152,56 @@ export function updateAlertNodes(ruleID, alerts) {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateRuleName(ruleID, name) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_NAME',
|
||||
payload: {
|
||||
ruleID,
|
||||
name,
|
||||
},
|
||||
}
|
||||
export const updateRuleName = (ruleID, name) => ({
|
||||
type: 'UPDATE_RULE_NAME',
|
||||
payload: {
|
||||
ruleID,
|
||||
name,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteRuleSuccess = ruleID => ({
|
||||
type: 'DELETE_RULE_SUCCESS',
|
||||
payload: {
|
||||
ruleID,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateRuleStatusSuccess = (ruleID, status) => ({
|
||||
type: 'UPDATE_RULE_STATUS_SUCCESS',
|
||||
payload: {
|
||||
ruleID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteRule = rule => dispatch => {
|
||||
deleteRuleAPI(rule)
|
||||
.then(() => {
|
||||
dispatch(deleteRuleSuccess(rule.id))
|
||||
dispatch(
|
||||
publishNotification('success', `${rule.name} deleted successfully`)
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
publishNotification('error', `${rule.name} could not be deleted`)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRuleSuccess(ruleID) {
|
||||
return {
|
||||
type: 'DELETE_RULE_SUCCESS',
|
||||
payload: {
|
||||
ruleID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRuleStatusSuccess(ruleID, status) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_STATUS_SUCCESS',
|
||||
payload: {
|
||||
ruleID,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteRule(rule) {
|
||||
return dispatch => {
|
||||
deleteRuleAPI(rule)
|
||||
.then(() => {
|
||||
dispatch(deleteRuleSuccess(rule.id))
|
||||
dispatch(
|
||||
publishNotification('success', `${rule.name} deleted successfully`)
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
publishNotification('error', `${rule.name} could not be deleted`)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRuleStatus(rule, status) {
|
||||
return dispatch => {
|
||||
updateRuleStatusAPI(rule, status)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
publishNotification('success', `${rule.name} ${status} successfully`)
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
publishNotification('error', `${rule.name} could not be ${status}`)
|
||||
)
|
||||
})
|
||||
}
|
||||
export const updateRuleStatus = (rule, status) => dispatch => {
|
||||
updateRuleStatusAPI(rule, status)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
publishNotification('success', `${rule.name} ${status} successfully`)
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
publishNotification('error', `${rule.name} could not be ${status}`)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const createTask = (
|
||||
|
|
|
@ -100,3 +100,41 @@ export const updateTask = async (
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const kapacitorLogHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
|
||||
export const getLogStream = kapacitor =>
|
||||
fetch(`${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs`, {
|
||||
method: 'GET',
|
||||
headers: kapacitorLogHeaders,
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
export const getLogStreamByRuleID = (kapacitor, ruleID) =>
|
||||
fetch(
|
||||
`${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs?task=${ruleID}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: kapacitorLogHeaders,
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
|
||||
export const pingKapacitorVersion = async kapacitor => {
|
||||
try {
|
||||
const result = await AJAX({
|
||||
method: 'GET',
|
||||
url: `${kapacitor.links.proxy}?path=/kapacitor/v1preview/ping`,
|
||||
headers: kapacitorLogHeaders,
|
||||
credentials: 'include',
|
||||
})
|
||||
const kapVersion = result.headers['x-kapacitor-version']
|
||||
return kapVersion === '' ? null : kapVersion
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemHTTP = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">HTTP Request</div>
|
||||
<div className="logs-table--http">
|
||||
{logItem.method} {logItem.username}@{logItem.host} ({logItem.duration})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemHTTP.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
method: string.isRequired,
|
||||
username: string.isRequired,
|
||||
host: string.isRequired,
|
||||
duration: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemHTTP
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemHTTPError = ({logItem}) =>
|
||||
<div className="logs-table--row" key={logItem.key}>
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">HTTP Server</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemHTTPError.propTypes = {
|
||||
logItem: shape({
|
||||
key: string.isRequired,
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemHTTPError
|
|
@ -0,0 +1,34 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemInfluxDBDebug = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">InfluxDB</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
<br />
|
||||
Cluster: {logItem.cluster}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemInfluxDBDebug.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
cluster: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemInfluxDBDebug
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemKapacitorDebug = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorDebug.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorDebug
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemKapacitorError = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorError.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorError
|
|
@ -0,0 +1,51 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const renderKeysAndValues = object => {
|
||||
if (!object) {
|
||||
return <span className="logs-table--empty-cell">--</span>
|
||||
}
|
||||
const objKeys = Object.keys(object)
|
||||
const objValues = Object.values(object)
|
||||
|
||||
const objElements = objKeys.map((objKey, i) =>
|
||||
<div key={i} className="logs-table--key-value">
|
||||
{objKey}: <span>{objValues[i]}</span>
|
||||
</div>
|
||||
)
|
||||
return objElements
|
||||
}
|
||||
const LogItemKapacitorPoint = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">Kapacitor Point</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values">
|
||||
TAGS<br />
|
||||
{renderKeysAndValues(logItem.tag)}
|
||||
</div>
|
||||
<div className="logs-table--key-values">
|
||||
FIELDS<br />
|
||||
{renderKeysAndValues(logItem.field)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorPoint.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
tag: shape.isRequired,
|
||||
field: shape.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorPoint
|
|
@ -0,0 +1,28 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemSession = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--session">
|
||||
{logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemSession.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemSession
|
|
@ -0,0 +1,38 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import LogsTableRow from 'src/kapacitor/components/LogsTableRow'
|
||||
|
||||
const LogsTable = ({logs}) =>
|
||||
<div className="logs-table--container">
|
||||
<div className="logs-table--header">
|
||||
<h2 className="panel-title">Logs</h2>
|
||||
</div>
|
||||
<FancyScrollbar
|
||||
className="logs-table--panel fancy-scroll--kapacitor"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="logs-table">
|
||||
{logs.length
|
||||
? logs.map((log, i) =>
|
||||
<LogsTableRow key={log.key} logItem={log} index={i} />
|
||||
)
|
||||
: <div className="page-spinner" />}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
|
||||
const {arrayOf, shape, string} = PropTypes
|
||||
|
||||
LogsTable.propTypes = {
|
||||
logs: arrayOf(
|
||||
shape({
|
||||
key: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
lvl: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
export default LogsTable
|
|
@ -0,0 +1,68 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import LogItemSession from 'src/kapacitor/components/LogItemSession'
|
||||
import LogItemHTTP from 'src/kapacitor/components/LogItemHTTP'
|
||||
import LogItemHTTPError from 'src/kapacitor/components/LogItemHTTPError'
|
||||
import LogItemKapacitorPoint from 'src/kapacitor/components/LogItemKapacitorPoint'
|
||||
import LogItemKapacitorError from 'src/kapacitor/components/LogItemKapacitorError'
|
||||
import LogItemKapacitorDebug from 'src/kapacitor/components/LogItemKapacitorDebug'
|
||||
import LogItemInfluxDBDebug from 'src/kapacitor/components/LogItemInfluxDBDebug'
|
||||
|
||||
const LogsTableRow = ({logItem, index}) => {
|
||||
if (logItem.service === 'sessions') {
|
||||
return <LogItemSession logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'http' && logItem.msg === 'http request') {
|
||||
return <LogItemHTTP logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.msg === 'point') {
|
||||
return <LogItemKapacitorPoint logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'httpd_server_errors' && logItem.lvl === 'error') {
|
||||
return <LogItemHTTPError logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'error') {
|
||||
return <LogItemKapacitorError logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'debug') {
|
||||
return <LogItemKapacitorDebug logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'influxdb' && logItem.lvl === 'debug') {
|
||||
return <LogItemInfluxDBDebug logItem={logItem} key={index} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs-table--row" key={index}>
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">
|
||||
{logItem.service || '--'}
|
||||
</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values">
|
||||
{logItem.msg || '--'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {number, shape, string} = PropTypes
|
||||
|
||||
LogsTableRow.propTypes = {
|
||||
logItem: shape({
|
||||
key: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
lvl: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}).isRequired,
|
||||
index: number,
|
||||
}
|
||||
|
||||
export default LogsTableRow
|
|
@ -0,0 +1,26 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) =>
|
||||
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle">
|
||||
<li
|
||||
className={areLogsVisible ? null : 'active'}
|
||||
onClick={onToggleLogsVisbility}
|
||||
>
|
||||
Editor
|
||||
</li>
|
||||
<li
|
||||
className={areLogsVisible ? 'active' : null}
|
||||
onClick={onToggleLogsVisbility}
|
||||
>
|
||||
Editor + Logs
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
const {bool, func} = PropTypes
|
||||
|
||||
LogsToggle.propTypes = {
|
||||
areLogsVisible: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
}
|
||||
|
||||
export default LogsToggle
|
|
@ -2,56 +2,63 @@ import React, {PropTypes} from 'react'
|
|||
|
||||
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
|
||||
import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
|
||||
import TickscriptEditorControls from 'src/kapacitor/components/TickscriptEditorControls'
|
||||
import TickscriptEditorConsole from 'src/kapacitor/components/TickscriptEditorConsole'
|
||||
import LogsTable from 'src/kapacitor/components/LogsTable'
|
||||
|
||||
const Tickscript = ({
|
||||
source,
|
||||
onSave,
|
||||
task,
|
||||
logs,
|
||||
validation,
|
||||
onSelectDbrps,
|
||||
onChangeScript,
|
||||
onChangeType,
|
||||
onChangeID,
|
||||
isNewTickscript,
|
||||
areLogsVisible,
|
||||
areLogsEnabled,
|
||||
onToggleLogsVisbility,
|
||||
}) =>
|
||||
<div className="page">
|
||||
<TickscriptHeader
|
||||
task={task}
|
||||
source={source}
|
||||
onSave={onSave}
|
||||
onChangeID={onChangeID}
|
||||
onChangeType={onChangeType}
|
||||
onSelectDbrps={onSelectDbrps}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
isNewTickscript={isNewTickscript}
|
||||
/>
|
||||
<div className="page-contents">
|
||||
<div className="tickscript-console">
|
||||
<div className="tickscript-console--output">
|
||||
{validation
|
||||
? <p>
|
||||
{validation}
|
||||
</p>
|
||||
: <p className="tickscript-console--default">
|
||||
Save your TICKscript to validate it
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tickscript-editor">
|
||||
<div className="page-contents--split">
|
||||
<div className="tickscript">
|
||||
<TickscriptEditorControls
|
||||
isNewTickscript={isNewTickscript}
|
||||
onSelectDbrps={onSelectDbrps}
|
||||
onChangeType={onChangeType}
|
||||
onChangeID={onChangeID}
|
||||
task={task}
|
||||
/>
|
||||
<TickscriptEditorConsole validation={validation} />
|
||||
<TickscriptEditor
|
||||
script={task.tickscript}
|
||||
onChangeScript={onChangeScript}
|
||||
/>
|
||||
</div>
|
||||
{areLogsVisible ? <LogsTable logs={logs} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
Tickscript.propTypes = {
|
||||
logs: arrayOf(shape()).isRequired,
|
||||
onSave: func.isRequired,
|
||||
source: shape({
|
||||
id: string,
|
||||
}),
|
||||
areLogsVisible: bool,
|
||||
areLogsEnabled: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
task: shape({
|
||||
id: string,
|
||||
script: string,
|
||||
|
|
|
@ -21,7 +21,13 @@ class TickscriptEditor extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<CodeMirror value={script} onChange={this.updateCode} options={options} />
|
||||
<div className="tickscript-editor">
|
||||
<CodeMirror
|
||||
value={script}
|
||||
onChange={this.updateCode}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const TickscriptEditorConsole = ({validation}) =>
|
||||
<div className="tickscript-console">
|
||||
<div className="tickscript-console--output">
|
||||
{validation
|
||||
? <p>
|
||||
{validation}
|
||||
</p>
|
||||
: <p className="tickscript-console--default">
|
||||
Save your TICKscript to validate it
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {string} = PropTypes
|
||||
|
||||
TickscriptEditorConsole.propTypes = {
|
||||
validation: string,
|
||||
}
|
||||
|
||||
export default TickscriptEditorConsole
|
|
@ -0,0 +1,44 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import TickscriptType from 'src/kapacitor/components/TickscriptType'
|
||||
import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
|
||||
import TickscriptID, {
|
||||
TickscriptStaticID,
|
||||
} from 'src/kapacitor/components/TickscriptID'
|
||||
|
||||
const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
|
||||
|
||||
const TickscriptEditorControls = ({
|
||||
isNewTickscript,
|
||||
onSelectDbrps,
|
||||
onChangeType,
|
||||
onChangeID,
|
||||
task,
|
||||
}) =>
|
||||
<div className="tickscript-controls">
|
||||
{isNewTickscript
|
||||
? <TickscriptID onChangeID={onChangeID} id={task.id} />
|
||||
: <TickscriptStaticID id={task.name} />}
|
||||
<div className="tickscript-controls--right">
|
||||
<TickscriptType type={task.type} onChangeType={onChangeType} />
|
||||
<MultiSelectDBDropdown
|
||||
selectedItems={addName(task.dbrps)}
|
||||
onApply={onSelectDbrps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
TickscriptEditorControls.propTypes = {
|
||||
isNewTickscript: bool.isRequired,
|
||||
onSelectDbrps: func.isRequired,
|
||||
onChangeType: func.isRequired,
|
||||
onChangeID: func.isRequired,
|
||||
task: shape({
|
||||
id: string,
|
||||
script: string,
|
||||
dbsrps: arrayOf(shape()),
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default TickscriptEditorControls
|
|
@ -1,52 +1,36 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import TickscriptType from 'src/kapacitor/components/TickscriptType'
|
||||
import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
|
||||
import TickscriptID, {
|
||||
TickscriptStaticID,
|
||||
} from 'src/kapacitor/components/TickscriptID'
|
||||
|
||||
const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
|
||||
import LogsToggle from 'src/kapacitor/components/LogsToggle'
|
||||
|
||||
const TickscriptHeader = ({
|
||||
task: {id, type, dbrps},
|
||||
task,
|
||||
source,
|
||||
task: {id},
|
||||
onSave,
|
||||
onChangeType,
|
||||
onChangeID,
|
||||
onSelectDbrps,
|
||||
areLogsVisible,
|
||||
areLogsEnabled,
|
||||
isNewTickscript,
|
||||
onToggleLogsVisbility,
|
||||
}) =>
|
||||
<div className="page-header">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
{isNewTickscript
|
||||
? <TickscriptID onChangeID={onChangeID} id={id} />
|
||||
: <TickscriptStaticID id={task.name} />}
|
||||
<h1 className="page-header__title">TICKscript Editor</h1>
|
||||
</div>
|
||||
{areLogsEnabled &&
|
||||
<LogsToggle
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
/>}
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<TickscriptType type={type} onChangeType={onChangeType} />
|
||||
<MultiSelectDBDropdown
|
||||
selectedItems={addName(dbrps)}
|
||||
onApply={onSelectDbrps}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-sm btn-default"
|
||||
to={`/sources/${source.id}/alert-rules`}
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
title={id ? '' : 'ID your TICKscript to save'}
|
||||
onClick={onSave}
|
||||
disabled={!id}
|
||||
>
|
||||
Save Rule
|
||||
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,11 +39,11 @@ const TickscriptHeader = ({
|
|||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
TickscriptHeader.propTypes = {
|
||||
isNewTickscript: bool,
|
||||
onSave: func,
|
||||
source: shape({
|
||||
id: string,
|
||||
}),
|
||||
onSelectDbrps: func.isRequired,
|
||||
areLogsVisible: bool,
|
||||
areLogsEnabled: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
task: shape({
|
||||
dbrps: arrayOf(
|
||||
shape({
|
||||
|
@ -68,9 +52,6 @@ TickscriptHeader.propTypes = {
|
|||
})
|
||||
),
|
||||
}),
|
||||
onChangeType: func.isRequired,
|
||||
onChangeID: func.isRequired,
|
||||
isNewTickscript: bool.isRequired,
|
||||
}
|
||||
|
||||
export default TickscriptHeader
|
||||
|
|
|
@ -10,7 +10,7 @@ class TickscriptID extends Component {
|
|||
|
||||
return (
|
||||
<input
|
||||
className="page-header--editing kapacitor-theme"
|
||||
className="form-control input-sm form-malachite"
|
||||
autoFocus={true}
|
||||
value={id}
|
||||
onChange={onChangeID}
|
||||
|
@ -23,10 +23,7 @@ class TickscriptID extends Component {
|
|||
}
|
||||
|
||||
export const TickscriptStaticID = ({id}) =>
|
||||
<h1
|
||||
className="page-header--editing kapacitor-theme"
|
||||
style={{display: 'flex', justifyContent: 'baseline'}}
|
||||
>
|
||||
<h1 className="tickscript-controls--name">
|
||||
{id}
|
||||
</h1>
|
||||
|
||||
|
|
|
@ -54,13 +54,13 @@ class KapacitorRulePage extends Component {
|
|||
render() {
|
||||
const {
|
||||
rules,
|
||||
queryConfigs,
|
||||
params,
|
||||
ruleActions,
|
||||
source,
|
||||
queryConfigActions,
|
||||
addFlashMessage,
|
||||
router,
|
||||
ruleActions,
|
||||
queryConfigs,
|
||||
addFlashMessage,
|
||||
queryConfigActions,
|
||||
} = this.props
|
||||
const {handlersFromConfig, kapacitor} = this.state
|
||||
const rule =
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import Tickscript from 'src/kapacitor/components/Tickscript'
|
||||
import * as kapactiorActionCreators from 'src/kapacitor/actions/view'
|
||||
import * as errorActionCreators from 'shared/actions/errors'
|
||||
import {getActiveKapacitor} from 'src/shared/apis'
|
||||
import {getLogStreamByRuleID, pingKapacitorVersion} from 'src/kapacitor/apis'
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
|
||||
class TickscriptPage extends Component {
|
||||
constructor(props) {
|
||||
|
@ -23,6 +26,96 @@ class TickscriptPage extends Component {
|
|||
},
|
||||
validation: '',
|
||||
isEditingID: true,
|
||||
logs: [],
|
||||
areLogsEnabled: false,
|
||||
failStr: '',
|
||||
}
|
||||
}
|
||||
|
||||
fetchChunkedLogs = async (kapacitor, ruleID) => {
|
||||
const {notify} = this.props
|
||||
|
||||
try {
|
||||
const version = await pingKapacitorVersion(kapacitor)
|
||||
|
||||
if (version && parseInt(version.split('.')[1], 10) < 4) {
|
||||
this.setState({
|
||||
areLogsEnabled: false,
|
||||
})
|
||||
notify(
|
||||
'warning',
|
||||
'Could not use logging, requires Kapacitor version 1.4'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.state.logs.length === 0) {
|
||||
this.setState({
|
||||
areLogsEnabled: true,
|
||||
logs: [
|
||||
{
|
||||
id: uuid.v4(),
|
||||
key: uuid.v4(),
|
||||
lvl: 'info',
|
||||
msg: 'created log session',
|
||||
service: 'sessions',
|
||||
tags: 'nil',
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const response = await getLogStreamByRuleID(kapacitor, ruleID)
|
||||
|
||||
const reader = await response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
let result
|
||||
|
||||
while (this.state.areLogsEnabled === true && !(result && result.done)) {
|
||||
result = await reader.read()
|
||||
|
||||
const chunk = decoder.decode(result.value || new Uint8Array(), {
|
||||
stream: !result.done,
|
||||
})
|
||||
|
||||
const json = chunk.split('\n')
|
||||
|
||||
let logs = []
|
||||
let failStr = this.state.failStr
|
||||
|
||||
try {
|
||||
for (let objStr of json) {
|
||||
objStr = failStr + objStr
|
||||
failStr = objStr
|
||||
const jsonStr = `[${objStr.split('}{').join('},{')}]`
|
||||
logs = [
|
||||
...logs,
|
||||
...JSON.parse(jsonStr).map(log => ({
|
||||
...log,
|
||||
key: uuid.v4(),
|
||||
})),
|
||||
]
|
||||
failStr = ''
|
||||
}
|
||||
|
||||
this.setState({
|
||||
logs: [...this.state.logs, ...logs],
|
||||
failStr,
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn(err, failStr)
|
||||
this.setState({
|
||||
logs: [...this.state.logs, ...logs],
|
||||
failStr,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notify('error', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,9 +143,17 @@ class TickscriptPage extends Component {
|
|||
this.setState({task: {tickscript, dbrps, type, status, name, id}})
|
||||
}
|
||||
|
||||
this.fetchChunkedLogs(kapacitor, ruleID)
|
||||
|
||||
this.setState({kapacitor})
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.setState({
|
||||
areLogsEnabled: false,
|
||||
})
|
||||
}
|
||||
|
||||
handleSave = async () => {
|
||||
const {kapacitor, task} = this.state
|
||||
const {
|
||||
|
@ -96,13 +197,18 @@ class TickscriptPage extends Component {
|
|||
this.setState({task: {...this.state.task, id: e.target.value}})
|
||||
}
|
||||
|
||||
handleToggleLogsVisbility = () => {
|
||||
this.setState({areLogsVisible: !this.state.areLogsVisible})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source} = this.props
|
||||
const {task, validation} = this.state
|
||||
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
|
||||
|
||||
return (
|
||||
<Tickscript
|
||||
task={task}
|
||||
logs={logs}
|
||||
source={source}
|
||||
validation={validation}
|
||||
onSave={this.handleSave}
|
||||
|
@ -111,6 +217,9 @@ class TickscriptPage extends Component {
|
|||
onChangeScript={this.handleChangeScript}
|
||||
onChangeType={this.handleChangeType}
|
||||
onChangeID={this.handleChangeID}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={this.handleToggleLogsVisbility}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -142,6 +251,7 @@ TickscriptPage.propTypes = {
|
|||
ruleID: string,
|
||||
}).isRequired,
|
||||
rules: arrayOf(shape()),
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
@ -153,6 +263,7 @@ const mapStateToProps = state => {
|
|||
const mapDispatchToProps = dispatch => ({
|
||||
kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch),
|
||||
errorActions: bindActionCreators(errorActionCreators, dispatch),
|
||||
notify: bindActionCreators(publishNotification, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
applyFuncsToField,
|
||||
chooseMeasurement,
|
||||
chooseNamespace,
|
||||
timeShift,
|
||||
chooseTag,
|
||||
groupByTag,
|
||||
groupByTime,
|
||||
removeFuncs,
|
||||
chooseNamespace,
|
||||
toggleKapaField,
|
||||
applyFuncsToField,
|
||||
chooseMeasurement,
|
||||
toggleTagAcceptance,
|
||||
} from 'src/utils/queryTransitions'
|
||||
|
||||
|
@ -34,9 +35,9 @@ const queryConfigs = (state = {}, action) => {
|
|||
}
|
||||
|
||||
case 'KAPA_CHOOSE_NAMESPACE': {
|
||||
const {queryId, database, retentionPolicy} = action.payload
|
||||
const {queryID, database, retentionPolicy} = action.payload
|
||||
const nextQueryConfig = chooseNamespace(
|
||||
state[queryId],
|
||||
state[queryID],
|
||||
{
|
||||
database,
|
||||
retentionPolicy,
|
||||
|
@ -45,75 +46,75 @@ const queryConfigs = (state = {}, action) => {
|
|||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
[queryID]: Object.assign(nextQueryConfig, {rawText: null}),
|
||||
})
|
||||
}
|
||||
|
||||
case 'KAPA_CHOOSE_MEASUREMENT': {
|
||||
const {queryId, measurement} = action.payload
|
||||
const {queryID, measurement} = action.payload
|
||||
const nextQueryConfig = chooseMeasurement(
|
||||
state[queryId],
|
||||
state[queryID],
|
||||
measurement,
|
||||
IS_KAPACITOR_RULE
|
||||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: Object.assign(nextQueryConfig, {
|
||||
rawText: state[queryId].rawText,
|
||||
[queryID]: Object.assign(nextQueryConfig, {
|
||||
rawText: state[queryID].rawText,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
case 'KAPA_CHOOSE_TAG': {
|
||||
const {queryId, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryId], tag)
|
||||
const {queryID, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryID], tag)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'KAPA_GROUP_BY_TAG': {
|
||||
const {queryId, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryId], tagKey)
|
||||
const {queryID, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryID], tagKey)
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'KAPA_TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryId} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryId])
|
||||
const {queryID} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryID])
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'KAPA_TOGGLE_FIELD': {
|
||||
const {queryId, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleKapaField(state[queryId], fieldFunc)
|
||||
const {queryID, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleKapaField(state[queryID], fieldFunc)
|
||||
|
||||
return {...state, [queryId]: {...nextQueryConfig, rawText: null}}
|
||||
return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
|
||||
}
|
||||
|
||||
case 'KAPA_APPLY_FUNCS_TO_FIELD': {
|
||||
const {queryId, fieldFunc} = action.payload
|
||||
const {groupBy} = state[queryId]
|
||||
const nextQueryConfig = applyFuncsToField(state[queryId], fieldFunc, {
|
||||
const {queryID, fieldFunc} = action.payload
|
||||
const {groupBy} = state[queryID]
|
||||
const nextQueryConfig = applyFuncsToField(state[queryID], fieldFunc, {
|
||||
...groupBy,
|
||||
time: groupBy.time ? groupBy.time : '10s',
|
||||
})
|
||||
|
||||
return {...state, [queryId]: nextQueryConfig}
|
||||
return {...state, [queryID]: nextQueryConfig}
|
||||
}
|
||||
|
||||
case 'KAPA_GROUP_BY_TIME': {
|
||||
const {queryId, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryId], time)
|
||||
const {queryID, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryID], time)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
[queryID]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -124,6 +125,13 @@ const queryConfigs = (state = {}, action) => {
|
|||
// fields with no functions cannot have a group by time
|
||||
return {...state, [queryID]: nextQuery}
|
||||
}
|
||||
|
||||
case 'KAPA_TIME_SHIFT': {
|
||||
const {queryID, shift} = action.payload
|
||||
const nextQuery = timeShift(state[queryID], shift)
|
||||
|
||||
return {...state, [queryID]: nextQuery}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ export function showQueries(source, db) {
|
|||
return proxy({source, query, db})
|
||||
}
|
||||
|
||||
export function killQuery(source, queryId) {
|
||||
const query = `KILL QUERY ${queryId}`
|
||||
export function killQuery(source, queryID) {
|
||||
const query = `KILL QUERY ${queryID}`
|
||||
|
||||
return proxy({source, query})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
class ColorDropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMenu = () => {
|
||||
const {disabled} = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({visible: !this.state.visible})
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
handleColorClick = color => () => {
|
||||
this.props.onChoose(color)
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {visible} = this.state
|
||||
const {colors, selected, disabled} = this.props
|
||||
|
||||
const dropdownClassNames = visible
|
||||
? 'color-dropdown open'
|
||||
: 'color-dropdown'
|
||||
const toggleClassNames = classnames(
|
||||
'btn btn-sm btn-default color-dropdown--toggle',
|
||||
{active: visible, 'color-dropdown__disabled': disabled}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={dropdownClassNames}>
|
||||
<div
|
||||
className={toggleClassNames}
|
||||
onClick={this.handleToggleMenu}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: selected.hex}}
|
||||
/>
|
||||
<div className="color-dropdown--name">
|
||||
{selected.name}
|
||||
</div>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{visible
|
||||
? <div className="color-dropdown--menu">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true}>
|
||||
{colors.map((color, i) =>
|
||||
<div
|
||||
className={
|
||||
color.name === selected.name
|
||||
? 'color-dropdown--item active'
|
||||
: 'color-dropdown--item'
|
||||
}
|
||||
key={i}
|
||||
onClick={this.handleColorClick(color)}
|
||||
>
|
||||
<span
|
||||
className="color-dropdown--swatch"
|
||||
style={{backgroundColor: color.hex}}
|
||||
/>
|
||||
<span className="color-dropdown--name">
|
||||
{color.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
ColorDropdown.propTypes = {
|
||||
selected: shape({
|
||||
hex: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
onChoose: func.isRequired,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
hex: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
export default OnClickOutside(ColorDropdown)
|
|
@ -45,12 +45,20 @@ const DatabaseList = React.createClass({
|
|||
this.getDbRp()
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (_.isEqual(prevProps.querySource, this.props.querySource)) {
|
||||
componentDidUpdate({querySource: prevSource, query: prevQuery}) {
|
||||
const {querySource: nextSource, query: nextQuery} = this.props
|
||||
const differentSource = !_.isEqual(prevSource, nextSource)
|
||||
|
||||
if (prevQuery.rawText === nextQuery.rawText) {
|
||||
return
|
||||
}
|
||||
|
||||
this.getDbRp()
|
||||
const newMetaQuery =
|
||||
nextQuery.rawText && nextQuery.rawText.match(/^(create|drop)/i)
|
||||
|
||||
if (differentSource || newMetaQuery) {
|
||||
setTimeout(this.getDbRp, 100)
|
||||
}
|
||||
},
|
||||
|
||||
getDbRp() {
|
||||
|
|
|
@ -4,14 +4,15 @@ import classnames from 'classnames'
|
|||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
|
||||
const DeleteButton = ({onClickDelete, buttonSize}) =>
|
||||
const DeleteButton = ({onClickDelete, buttonSize, icon, square}) =>
|
||||
<button
|
||||
className={classnames('btn btn-danger table--show-on-row-hover', {
|
||||
[buttonSize]: buttonSize,
|
||||
'btn-square': square,
|
||||
})}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
Delete
|
||||
{icon ? <span className={`icon ${icon}`} /> : 'Delete'}
|
||||
</button>
|
||||
|
||||
class DeleteConfirmButtons extends Component {
|
||||
|
@ -37,7 +38,7 @@ class DeleteConfirmButtons extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {onDelete, item, buttonSize} = this.props
|
||||
const {onDelete, item, buttonSize, icon, square} = this.props
|
||||
const {isConfirming} = this.state
|
||||
|
||||
return isConfirming
|
||||
|
@ -50,21 +51,27 @@ class DeleteConfirmButtons extends Component {
|
|||
: <DeleteButton
|
||||
onClickDelete={this.handleClickDelete}
|
||||
buttonSize={buttonSize}
|
||||
icon={icon}
|
||||
square={square}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
const {func, oneOfType, shape, string} = PropTypes
|
||||
const {bool, func, oneOfType, shape, string} = PropTypes
|
||||
|
||||
DeleteButton.propTypes = {
|
||||
onClickDelete: func.isRequired,
|
||||
buttonSize: string,
|
||||
icon: string,
|
||||
square: bool,
|
||||
}
|
||||
|
||||
DeleteConfirmButtons.propTypes = {
|
||||
item: oneOfType([(string, shape())]),
|
||||
onDelete: func.isRequired,
|
||||
buttonSize: string,
|
||||
square: bool,
|
||||
icon: string,
|
||||
}
|
||||
|
||||
DeleteConfirmButtons.defaultProps = {
|
||||
|
|
|
@ -355,7 +355,8 @@ export default class Dygraph extends Component {
|
|||
}
|
||||
|
||||
highlightCallback = ({pageX}) => {
|
||||
this.setState({isHidden: false, pageX})
|
||||
this.pageX = pageX
|
||||
this.setState({isHidden: false})
|
||||
}
|
||||
|
||||
legendFormatter = legend => {
|
||||
|
@ -381,7 +382,6 @@ export default class Dygraph extends Component {
|
|||
render() {
|
||||
const {
|
||||
legend,
|
||||
pageX,
|
||||
sortType,
|
||||
isHidden,
|
||||
isSnipped,
|
||||
|
@ -396,7 +396,7 @@ export default class Dygraph extends Component {
|
|||
{...legend}
|
||||
graph={this.graphRef}
|
||||
legend={this.legendRef}
|
||||
pageX={pageX}
|
||||
pageX={this.pageX}
|
||||
sortType={sortType}
|
||||
onHide={this.handleHideLegend}
|
||||
isHidden={isHidden}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import QueryOptions from 'shared/components/QueryOptions'
|
||||
import FieldListItem from 'src/data_explorer/components/FieldListItem'
|
||||
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
|
||||
import FillQuery from 'shared/components/FillQuery'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {showFieldKeys} from 'shared/apis/metaQuery'
|
||||
|
@ -107,6 +106,10 @@ class FieldList extends Component {
|
|||
applyFuncsToField(fieldFunc, groupBy)
|
||||
}
|
||||
|
||||
handleTimeShift = shift => {
|
||||
this.props.onTimeShift(shift)
|
||||
}
|
||||
|
||||
_getFields = () => {
|
||||
const {database, measurement, retentionPolicy} = this.props.query
|
||||
const {source} = this.context
|
||||
|
@ -129,12 +132,11 @@ class FieldList extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
query: {database, measurement, fields = [], groupBy, fill},
|
||||
query: {database, measurement, fields = [], groupBy, fill, shifts},
|
||||
isKapacitorRule,
|
||||
} = this.props
|
||||
|
||||
const hasAggregates = numFunctions(fields) > 0
|
||||
const hasGroupByTime = groupBy.time
|
||||
const noDBorMeas = !database || !measurement
|
||||
|
||||
return (
|
||||
|
@ -142,16 +144,15 @@ class FieldList extends Component {
|
|||
<div className="query-builder--heading">
|
||||
<span>Fields</span>
|
||||
{hasAggregates
|
||||
? <div className="query-builder--groupby-fill-container">
|
||||
<GroupByTimeDropdown
|
||||
isOpen={!hasGroupByTime}
|
||||
selected={groupBy.time}
|
||||
onChooseGroupByTime={this.handleGroupByTime}
|
||||
/>
|
||||
{isKapacitorRule
|
||||
? null
|
||||
: <FillQuery value={fill} onChooseFill={this.handleFill} />}
|
||||
</div>
|
||||
? <QueryOptions
|
||||
fill={fill}
|
||||
shift={_.first(shifts)}
|
||||
groupBy={groupBy}
|
||||
onFill={this.handleFill}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
onTimeShift={this.handleTimeShift}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
{noDBorMeas
|
||||
|
@ -192,7 +193,7 @@ class FieldList extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
FieldList.defaultProps = {
|
||||
isKapacitorRule: false,
|
||||
|
@ -212,7 +213,15 @@ FieldList.propTypes = {
|
|||
database: string,
|
||||
retentionPolicy: string,
|
||||
measurement: string,
|
||||
shifts: arrayOf(
|
||||
shape({
|
||||
label: string,
|
||||
unit: string,
|
||||
quantity: string,
|
||||
})
|
||||
),
|
||||
}).isRequired,
|
||||
onTimeShift: func,
|
||||
onToggleField: func.isRequired,
|
||||
onGroupByTime: func.isRequired,
|
||||
onFill: func,
|
||||
|
|
|
@ -0,0 +1,340 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {GAUGE_SPECS} from 'shared/constants/gaugeSpecs'
|
||||
|
||||
import {
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
MIN_THRESHOLDS,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
class Gauge extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateCanvas()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateCanvas()
|
||||
}
|
||||
|
||||
resetCanvas = (canvas, context) => {
|
||||
context.setTransform(1, 0, 0, 1, 0, 0)
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
updateCanvas = () => {
|
||||
const canvas = this.canvasRef
|
||||
canvas.width = canvas.height * (canvas.clientWidth / canvas.clientHeight)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
this.resetCanvas(canvas, ctx)
|
||||
|
||||
const centerX = canvas.width / 2
|
||||
const centerY = canvas.height / 2 * 1.13
|
||||
const radius = Math.min(canvas.width, canvas.height) / 2 * 0.5
|
||||
|
||||
const {minLineWidth, minFontSize} = GAUGE_SPECS
|
||||
const gradientThickness = Math.max(minLineWidth, radius / 4)
|
||||
const labelValueFontSize = Math.max(minFontSize, radius / 4)
|
||||
|
||||
const {colors} = this.props
|
||||
if (!colors || colors.length === 0) {
|
||||
return
|
||||
}
|
||||
// Distill out max and min values
|
||||
const minValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
||||
)
|
||||
const maxValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
||||
)
|
||||
|
||||
// The following functions must be called in the specified order
|
||||
if (colors.length === MIN_THRESHOLDS) {
|
||||
this.drawGradientGauge(ctx, centerX, centerY, radius, gradientThickness)
|
||||
} else {
|
||||
this.drawSegmentedGauge(
|
||||
ctx,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
minValue,
|
||||
maxValue,
|
||||
gradientThickness
|
||||
)
|
||||
}
|
||||
this.drawGaugeLines(ctx, centerX, centerY, radius, gradientThickness)
|
||||
this.drawGaugeLabels(
|
||||
ctx,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
gradientThickness,
|
||||
minValue,
|
||||
maxValue
|
||||
)
|
||||
this.drawGaugeValue(ctx, radius, labelValueFontSize)
|
||||
this.drawNeedle(ctx, radius, minValue, maxValue)
|
||||
}
|
||||
|
||||
drawGradientGauge = (ctx, xc, yc, r, gradientThickness) => {
|
||||
const {colors} = this.props
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
const arcStart = Math.PI * 0.75
|
||||
const arcEnd = arcStart + Math.PI * 1.5
|
||||
|
||||
// Determine coordinates for gradient
|
||||
const xStart = xc + Math.cos(arcStart) * r
|
||||
const yStart = yc + Math.sin(arcStart) * r
|
||||
const xEnd = xc + Math.cos(arcEnd) * r
|
||||
const yEnd = yc + Math.sin(arcEnd) * r
|
||||
|
||||
const gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd)
|
||||
gradient.addColorStop(0, sortedColors[0].hex)
|
||||
gradient.addColorStop(1.0, sortedColors[1].hex)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = gradientThickness
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.arc(xc, yc, r, arcStart, arcEnd)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
drawSegmentedGauge = (
|
||||
ctx,
|
||||
xc,
|
||||
yc,
|
||||
r,
|
||||
minValue,
|
||||
maxValue,
|
||||
gradientThickness
|
||||
) => {
|
||||
const {colors} = this.props
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
const trueValueRange = Math.abs(maxValue - minValue)
|
||||
const totalArcLength = Math.PI * 1.5
|
||||
let startingPoint = Math.PI * 0.75
|
||||
|
||||
// Iterate through colors, draw arc for each
|
||||
for (let c = 0; c < sortedColors.length - 1; c++) {
|
||||
// Use this color and the next to determine arc length
|
||||
const color = sortedColors[c]
|
||||
const nextColor = sortedColors[c + 1]
|
||||
|
||||
// adjust values by subtracting minValue from them
|
||||
const adjustedValue = Number(color.value) - minValue
|
||||
const adjustedNextValue = Number(nextColor.value) - minValue
|
||||
|
||||
const thisArc = Math.abs(adjustedValue - adjustedNextValue)
|
||||
// Multiply by arcLength to determine this arc's length
|
||||
const arcLength = totalArcLength * (thisArc / trueValueRange)
|
||||
// Draw arc
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = gradientThickness
|
||||
ctx.strokeStyle = color.hex
|
||||
ctx.arc(xc, yc, r, startingPoint, startingPoint + arcLength)
|
||||
ctx.stroke()
|
||||
// Add this arc's length to starting point
|
||||
startingPoint += arcLength
|
||||
}
|
||||
}
|
||||
|
||||
drawGaugeLines = (ctx, xc, yc, radius, gradientThickness) => {
|
||||
const {
|
||||
degree,
|
||||
lineCount,
|
||||
lineColor,
|
||||
lineStrokeSmall,
|
||||
lineStrokeLarge,
|
||||
tickSizeSmall,
|
||||
tickSizeLarge,
|
||||
} = GAUGE_SPECS
|
||||
|
||||
const arcStart = Math.PI * 0.75
|
||||
const arcLength = Math.PI * 1.5
|
||||
const arcStop = arcStart + arcLength
|
||||
const lineSmallCount = lineCount * 5
|
||||
const startDegree = degree * 135
|
||||
const arcLargeIncrement = arcLength / lineCount
|
||||
const arcSmallIncrement = arcLength / lineSmallCount
|
||||
|
||||
// Semi-circle
|
||||
const arcRadius = radius + gradientThickness * 0.8
|
||||
ctx.beginPath()
|
||||
ctx.arc(xc, yc, arcRadius, arcStart, arcStop)
|
||||
ctx.lineWidth = 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.strokeStyle = lineColor
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
|
||||
// Match center of canvas to center of gauge
|
||||
ctx.translate(xc, yc)
|
||||
|
||||
// Draw Large ticks
|
||||
for (let lt = 0; lt <= lineCount; lt++) {
|
||||
// Rototion before drawing line
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(lt * arcLargeIncrement)
|
||||
// Draw line
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = lineStrokeLarge
|
||||
ctx.lineCap = 'round'
|
||||
ctx.strokeStyle = lineColor
|
||||
ctx.moveTo(arcRadius, 0)
|
||||
ctx.lineTo(arcRadius + tickSizeLarge, 0)
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
// Return to starting rotation
|
||||
ctx.rotate(-lt * arcLargeIncrement)
|
||||
ctx.rotate(-startDegree)
|
||||
}
|
||||
|
||||
// Draw Small ticks
|
||||
for (let lt = 0; lt <= lineSmallCount; lt++) {
|
||||
// Rototion before drawing line
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(lt * arcSmallIncrement)
|
||||
// Draw line
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = lineStrokeSmall
|
||||
ctx.lineCap = 'round'
|
||||
ctx.strokeStyle = lineColor
|
||||
ctx.moveTo(arcRadius, 0)
|
||||
ctx.lineTo(arcRadius + tickSizeSmall, 0)
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
// Return to starting rotation
|
||||
ctx.rotate(-lt * arcSmallIncrement)
|
||||
ctx.rotate(-startDegree)
|
||||
}
|
||||
}
|
||||
|
||||
drawGaugeLabels = (
|
||||
ctx,
|
||||
xc,
|
||||
yc,
|
||||
radius,
|
||||
gradientThickness,
|
||||
minValue,
|
||||
maxValue
|
||||
) => {
|
||||
const {degree, lineCount, labelColor, labelFontSize} = GAUGE_SPECS
|
||||
|
||||
const incrementValue = (maxValue - minValue) / lineCount
|
||||
|
||||
const gaugeValues = []
|
||||
for (let g = minValue; g < maxValue; g += incrementValue) {
|
||||
const roundedValue = Math.round(g * 100) / 100
|
||||
gaugeValues.push(roundedValue.toString())
|
||||
}
|
||||
gaugeValues.push((Math.round(maxValue * 100) / 100).toString())
|
||||
|
||||
const startDegree = degree * 135
|
||||
const arcLength = Math.PI * 1.5
|
||||
const arcIncrement = arcLength / lineCount
|
||||
|
||||
// Format labels text
|
||||
ctx.font = `bold ${labelFontSize}px Helvetica`
|
||||
ctx.fillStyle = labelColor
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'right'
|
||||
let labelRadius
|
||||
|
||||
for (let i = 0; i <= lineCount; i++) {
|
||||
if (i === 3) {
|
||||
ctx.textAlign = 'center'
|
||||
labelRadius = radius + gradientThickness + 30
|
||||
} else {
|
||||
labelRadius = radius + gradientThickness + 23
|
||||
}
|
||||
if (i > 3) {
|
||||
ctx.textAlign = 'left'
|
||||
}
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(i * arcIncrement)
|
||||
ctx.translate(labelRadius, 0)
|
||||
ctx.rotate(i * -arcIncrement)
|
||||
ctx.rotate(-startDegree)
|
||||
ctx.fillText(gaugeValues[i], 0, 0)
|
||||
ctx.rotate(startDegree)
|
||||
ctx.rotate(i * arcIncrement)
|
||||
ctx.translate(-labelRadius, 0)
|
||||
ctx.rotate(i * -arcIncrement)
|
||||
ctx.rotate(-startDegree)
|
||||
}
|
||||
}
|
||||
|
||||
drawGaugeValue = (ctx, radius, labelValueFontSize) => {
|
||||
const {gaugePosition} = this.props
|
||||
const {valueColor} = GAUGE_SPECS
|
||||
|
||||
ctx.font = `${labelValueFontSize}px Roboto`
|
||||
ctx.fillStyle = valueColor
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
|
||||
const textY = radius
|
||||
ctx.fillText(gaugePosition.toString(), 0, textY)
|
||||
}
|
||||
|
||||
drawNeedle = (ctx, radius, minValue, maxValue) => {
|
||||
const {gaugePosition} = this.props
|
||||
const {degree, needleColor0, needleColor1} = GAUGE_SPECS
|
||||
const arcDistance = Math.PI * 1.5
|
||||
|
||||
const needleRotation = (gaugePosition - minValue) / (maxValue - minValue)
|
||||
|
||||
const needleGradient = ctx.createLinearGradient(0, -10, 0, radius)
|
||||
needleGradient.addColorStop(0, needleColor0)
|
||||
needleGradient.addColorStop(1, needleColor1)
|
||||
|
||||
// Starting position of needle is at minimum
|
||||
ctx.rotate(degree * 45)
|
||||
ctx.rotate(arcDistance * needleRotation)
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = needleGradient
|
||||
ctx.arc(0, 0, 10, 0, Math.PI, true)
|
||||
ctx.lineTo(0, radius)
|
||||
ctx.lineTo(10, 0)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {width, height} = this.props
|
||||
return (
|
||||
<canvas
|
||||
className="gauge"
|
||||
width={width}
|
||||
height={height}
|
||||
ref={r => (this.canvasRef = r)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, number, shape, string} = PropTypes
|
||||
|
||||
Gauge.propTypes = {
|
||||
width: string.isRequired,
|
||||
height: string.isRequired,
|
||||
gaugePosition: number.isRequired,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
export default Gauge
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue