Merge branch 'master' into multiple-event-handlers

pull/10616/head
Deniz Kusefoglu 2017-12-05 16:18:01 -08:00 committed by GitHub
commit 903e461d40
135 changed files with 5121 additions and 1510 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 != "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -228,11 +228,7 @@ describe('timeSeriesToDygraph', () => {
]
const isInDataExplorer = true
const actual = timeSeriesToDygraph(
influxResponse,
undefined,
isInDataExplorer
)
const actual = timeSeriesToDygraph(influxResponse, isInDataExplorer)
const expected = {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ const OverlayControls = ({
})}
onClick={onClickDisplayOptions(true)}
>
Options
Visualization
</li>
</ul>
<div className="overlay-controls--right">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'], [])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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