Merge remote-tracking branch 'origin/master' into fix/ie11-support
commit
5777552296
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.3.2.0
|
||||
current_version = 1.3.5.0
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,11 +1,33 @@
|
|||
## v1.3.5.0 [unreleased]
|
||||
|
||||
## v1.3.6.0 [unreleased]
|
||||
### Bug Fixes
|
||||
1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu
|
||||
|
||||
### Features
|
||||
### UI Improvements
|
||||
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written
|
||||
|
||||
## v1.3.5.0 [2017-07-25]
|
||||
### Bug Fixes
|
||||
1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu
|
||||
1. [#1752](https://github.com/influxdata/chronograf/pull/1752): Clarify BoltPath server flag help text by making example the default path
|
||||
1. [#1703](https://github.com/influxdata/chronograf/pull/1703): Fix cell name cancel not reverting to original name
|
||||
1. [#1751](https://github.com/influxdata/chronograf/pull/1751): Fix typo that may have affected PagerDuty node creation in Kapacitor
|
||||
1. [#1756](https://github.com/influxdata/chronograf/pull/1756): Prevent 'auto' GROUP BY as option in Kapacitor rule builder when applying a function to a field
|
||||
1. [#1773](https://github.com/influxdata/chronograf/pull/1773): Prevent clipped buttons in Rule Builder, Data Explorer, and Configuration pages
|
||||
1. [#1776](https://github.com/influxdata/chronograf/pull/1776): Fix JWT for the write path
|
||||
1. [#1777](https://github.com/influxdata/chronograf/pull/1777): Disentangle client Kapacitor rule creation from Data Explorer query creation
|
||||
|
||||
### Features
|
||||
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
|
||||
1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards
|
||||
1. [#1752](https://github.com/influxdata/chronograf/pull/1752): Clarify BoltPath server flag help text by making example the default path
|
||||
1. [#1738](https://github.com/influxdata/chronograf/pull/1738): Add shared secret JWT authorization to InfluxDB
|
||||
1. [#1724](https://github.com/influxdata/chronograf/pull/1724): Add Pushover alert support
|
||||
1. [#1762](https://github.com/influxdata/chronograf/pull/1762): Restore all supported Kapacitor services when creating rules, and add most optional message parameters
|
||||
|
||||
### UI Improvements
|
||||
1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less
|
||||
1. [#1770](https://github.com/influxdata/chronograf/pull/1770): Specify that version is for Chronograf on Configuration page
|
||||
1. [#1779](https://github.com/influxdata/chronograf/pull/1779): Move custom time range indicator on cells into corner when in presentation mode
|
||||
1. [#1779](https://github.com/influxdata/chronograf/pull/1779): Highlight legend "Snip" toggle when active
|
||||
|
||||
## v1.3.4.0 [2017-07-10]
|
||||
### Bug Fixes
|
||||
|
@ -16,6 +38,7 @@
|
|||
|
||||
### Features
|
||||
1. [#1645](https://github.com/influxdata/chronograf/pull/1645): Add Auth0 as a supported OAuth2 provider
|
||||
1. [#1660](https://github.com/influxdata/chronograf/pull/1660): Add ability to add custom links to User menu via server CLI or ENV vars
|
||||
1. [#1660](https://github.com/influxdata/chronograf/pull/1660): Allow users to configure custom links on startup that will appear under the User menu in the sidebar
|
||||
1. [#1674](https://github.com/influxdata/chronograf/pull/1674): Add support for Auth0 organizations
|
||||
1. [#1695](https://github.com/influxdata/chronograf/pull/1695): Allow users to configure InfluxDB and Kapacitor sources on startup
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
memo = "bac138180cd86a0ae604cd3aa7b6ba300673478c880882bd58a4bd7f8bff518d"
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/NYTimes/gziphandler"
|
||||
|
@ -46,6 +47,11 @@ memo = "bac138180cd86a0ae604cd3aa7b6ba300673478c880882bd58a4bd7f8bff518d"
|
|||
packages = ["proto"]
|
||||
revision = "8ee79997227bf9b34611aee7946ae64735e6fd93"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = ["cmp"]
|
||||
revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-github"
|
||||
packages = ["github"]
|
||||
|
@ -63,9 +69,9 @@ memo = "bac138180cd86a0ae604cd3aa7b6ba300673478c880882bd58a4bd7f8bff518d"
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
packages = ["client/v1","influxdb","models","pipeline","services/k8s/client","tick","tick/ast","tick/stateful","udf"]
|
||||
revision = "5408057e5a3493d3b5bd38d5d535ea45b587f8ff"
|
||||
version = "v1.2.0"
|
||||
packages = ["client/v1","pipeline","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
|
||||
revision = "3b5512f7276483326577907803167e4bb213c613"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/usage-client"
|
||||
|
@ -130,3 +136,10 @@ memo = "bac138180cd86a0ae604cd3aa7b6ba300673478c880882bd58a4bd7f8bff518d"
|
|||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f34fb88755292baba8b52c14bf5b9a028daff96a763368a7cf1de90004d33695"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
38
Gopkg.toml
38
Gopkg.toml
|
@ -1,77 +1,77 @@
|
|||
required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/NYTimes/gziphandler"
|
||||
revision = "6710af535839f57c687b62c4c23d649f9545d885"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/Sirupsen/logrus"
|
||||
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/boltdb/bolt"
|
||||
revision = "5cc10bbbc5c141029940133bb33c9e969512a698"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/bouk/httprouter"
|
||||
revision = "ee8b3818a7f51fbc94cc709b5744b52c2c725e91"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
revision = "24c63f56522a87ec5339cc3567883f1039378fdb"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/elazarl/go-bindata-assetfs"
|
||||
revision = "9a6736ed45b44bf3835afeebb3034b57ed329f3e"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/google/go-github"
|
||||
revision = "1bc362c7737e51014af7299e016444b654095ad9"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
version = "^1.2.0"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/usage-client"
|
||||
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/jteeuwen/go-bindata"
|
||||
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
revision = "b061729afc07e77a8aa4fad0a2fd840958f1942a"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/sergi/go-diff"
|
||||
revision = "1d28411638c1e67fe1930830df207bef72496ae9"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "github.com/tylerb/graceful"
|
||||
version = "^1.2.13"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "golang.org/x/oauth2"
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
|
||||
[[dependencies]]
|
||||
[[constraint]]
|
||||
name = "google.golang.org/api"
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
||||
|
|
|
@ -110,7 +110,7 @@ Change the default root path of the Chronograf server with the `--basepath` opti
|
|||
|
||||
## Versions
|
||||
|
||||
The most recent version of Chronograf is [v1.3.2.0](https://www.influxdata.com/downloads/).
|
||||
The most recent version of Chronograf is [v1.3.5.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)!
|
||||
|
@ -138,7 +138,7 @@ By default, chronograf runs on port `8888`.
|
|||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
docker pull chronograf:1.3.2.0
|
||||
docker pull chronograf:1.3.5.0
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
|
|
@ -17,6 +17,7 @@ func MarshalSource(s chronograf.Source) ([]byte, error) {
|
|||
Type: s.Type,
|
||||
Username: s.Username,
|
||||
Password: s.Password,
|
||||
SharedSecret: s.SharedSecret,
|
||||
URL: s.URL,
|
||||
MetaURL: s.MetaURL,
|
||||
InsecureSkipVerify: s.InsecureSkipVerify,
|
||||
|
@ -37,6 +38,7 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error {
|
|||
s.Type = pb.Type
|
||||
s.Username = pb.Username
|
||||
s.Password = pb.Password
|
||||
s.SharedSecret = pb.SharedSecret
|
||||
s.URL = pb.URL
|
||||
s.MetaURL = pb.MetaURL
|
||||
s.InsecureSkipVerify = pb.InsecureSkipVerify
|
||||
|
@ -179,6 +181,19 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
}
|
||||
}
|
||||
|
||||
axes := make(map[string]*Axis, len(c.Axes))
|
||||
for a, r := range c.Axes {
|
||||
// need to explicitly allocate a new array because r.Bounds is
|
||||
// over-written and the resulting slices from previous iterations will
|
||||
// point to later iteration's data. It is _not_ enough to simply re-slice
|
||||
// r.Bounds
|
||||
axis := [2]int64{}
|
||||
copy(axis[:], r.Bounds[:2])
|
||||
axes[a] = &Axis{
|
||||
Bounds: axis[:],
|
||||
}
|
||||
}
|
||||
|
||||
cells[i] = &DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
|
@ -188,6 +203,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
Name: c.Name,
|
||||
Queries: queries,
|
||||
Type: c.Type,
|
||||
Axes: axes,
|
||||
}
|
||||
}
|
||||
templates := make([]*Template, len(d.Templates))
|
||||
|
@ -251,6 +267,13 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
}
|
||||
}
|
||||
|
||||
axes := make(map[string]chronograf.Axis, len(c.Axes))
|
||||
for a, r := range c.Axes {
|
||||
axis := chronograf.Axis{}
|
||||
copy(axis.Bounds[:], r.Bounds[:2])
|
||||
axes[a] = axis
|
||||
}
|
||||
|
||||
cells[i] = chronograf.DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
|
@ -260,6 +283,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
Name: c.Name,
|
||||
Queries: queries,
|
||||
Type: c.Type,
|
||||
Axes: axes,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ It has these top-level messages:
|
|||
Source
|
||||
Dashboard
|
||||
DashboardCell
|
||||
Axis
|
||||
Template
|
||||
TemplateValue
|
||||
TemplateQuery
|
||||
|
@ -51,6 +52,7 @@ type Source struct {
|
|||
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"`
|
||||
InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"`
|
||||
MetaURL string `protobuf:"bytes,10,opt,name=MetaURL,proto3" json:"MetaURL,omitempty"`
|
||||
SharedSecret string `protobuf:"bytes,11,opt,name=SharedSecret,proto3" json:"SharedSecret,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Source) Reset() { *m = Source{} }
|
||||
|
@ -85,14 +87,15 @@ func (m *Dashboard) GetTemplates() []*Template {
|
|||
}
|
||||
|
||||
type DashboardCell struct {
|
||||
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
||||
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
|
||||
W int32 `protobuf:"varint,3,opt,name=w,proto3" json:"w,omitempty"`
|
||||
H int32 `protobuf:"varint,4,opt,name=h,proto3" json:"h,omitempty"`
|
||||
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
|
||||
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
|
||||
ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
||||
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
|
||||
W int32 `protobuf:"varint,3,opt,name=w,proto3" json:"w,omitempty"`
|
||||
H int32 `protobuf:"varint,4,opt,name=h,proto3" json:"h,omitempty"`
|
||||
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
|
||||
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
|
||||
|
@ -107,6 +110,22 @@ func (m *DashboardCell) GetQueries() []*Query {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *DashboardCell) GetAxes() map[string]*Axis {
|
||||
if m != nil {
|
||||
return m.Axes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Axis struct {
|
||||
Bounds []int64 `protobuf:"varint,1,rep,name=bounds" json:"bounds,omitempty"`
|
||||
}
|
||||
|
||||
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} }
|
||||
|
||||
type Template struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
TempVar string `protobuf:"bytes,2,opt,name=temp_var,json=tempVar,proto3" json:"temp_var,omitempty"`
|
||||
|
@ -119,7 +138,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{3} }
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
|
||||
func (m *Template) GetValues() []*TemplateValue {
|
||||
if m != nil {
|
||||
|
@ -144,7 +163,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{4} }
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
type TemplateQuery struct {
|
||||
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
|
||||
|
@ -158,7 +177,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{5} }
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
|
||||
type Server struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -173,7 +192,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{6} }
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
|
||||
type Layout struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -186,7 +205,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{7} }
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
|
||||
func (m *Layout) GetCells() []*Cell {
|
||||
if m != nil {
|
||||
|
@ -211,7 +230,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{8} }
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
|
||||
func (m *Cell) GetQueries() []*Query {
|
||||
if m != nil {
|
||||
|
@ -233,7 +252,7 @@ type Query struct {
|
|||
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{9} }
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
|
||||
func (m *Query) GetRange() *Range {
|
||||
if m != nil {
|
||||
|
@ -250,7 +269,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{10} }
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
|
||||
type AlertRule struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -262,7 +281,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{11} }
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
|
||||
type User struct {
|
||||
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -272,12 +291,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{12} }
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
||||
proto.RegisterType((*Axis)(nil), "internal.Axis")
|
||||
proto.RegisterType((*Template)(nil), "internal.Template")
|
||||
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
|
||||
proto.RegisterType((*TemplateQuery)(nil), "internal.TemplateQuery")
|
||||
|
@ -293,59 +313,65 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 858 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
|
||||
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xee, 0x16, 0x34, 0x5a, 0xb1, 0x06, 0x6e, 0x22, 0x0b, 0xa4,
|
||||
0x82, 0x44, 0x41, 0xec, 0x13, 0xb4, 0xb5, 0x84, 0x42, 0xbb, 0x4b, 0x99, 0xb4, 0xe5, 0x0a, 0xad,
|
||||
0x26, 0xc9, 0x49, 0x6b, 0xed, 0x24, 0x36, 0x63, 0xbb, 0x59, 0xbf, 0x02, 0x57, 0x3c, 0x01, 0x12,
|
||||
0x12, 0x57, 0x5c, 0xf2, 0x02, 0x3c, 0x04, 0x2f, 0x84, 0xce, 0xcc, 0xf8, 0x27, 0x6c, 0x41, 0x7b,
|
||||
0xb5, 0x77, 0xf3, 0x9d, 0x33, 0xf9, 0xe6, 0xfc, 0x7c, 0x9f, 0x03, 0x87, 0xd9, 0xb6, 0x42, 0xbd,
|
||||
0x95, 0xea, 0xb8, 0xd0, 0x79, 0x95, 0xf3, 0xb0, 0xc5, 0xc9, 0xcf, 0x23, 0x18, 0xcf, 0xf3, 0x5a,
|
||||
0x2f, 0x91, 0x1f, 0xc2, 0x68, 0x96, 0xc6, 0x6c, 0xca, 0x8e, 0x3c, 0x31, 0x9a, 0xa5, 0x9c, 0x83,
|
||||
0xff, 0x42, 0x6e, 0x30, 0x1e, 0x4d, 0xd9, 0x51, 0x24, 0xcc, 0x99, 0x62, 0x57, 0x4d, 0x81, 0xb1,
|
||||
0x67, 0x63, 0x74, 0xe6, 0x1f, 0x41, 0x78, 0x5d, 0x12, 0xdb, 0x06, 0x63, 0xdf, 0xc4, 0x3b, 0x4c,
|
||||
0xb9, 0x4b, 0x59, 0x96, 0xbb, 0x5c, 0xaf, 0xe2, 0xc0, 0xe6, 0x5a, 0xcc, 0xdf, 0x07, 0xef, 0x5a,
|
||||
0x5c, 0xc4, 0x63, 0x13, 0xa6, 0x23, 0x8f, 0x61, 0x92, 0xe2, 0x5a, 0xd6, 0xaa, 0x8a, 0x27, 0x53,
|
||||
0x76, 0x14, 0x8a, 0x16, 0x12, 0xcf, 0x15, 0x2a, 0xbc, 0xd5, 0x72, 0x1d, 0x87, 0x96, 0xa7, 0xc5,
|
||||
0xfc, 0x18, 0xf8, 0x6c, 0x5b, 0xe2, 0xb2, 0xd6, 0x38, 0x7f, 0x95, 0x15, 0x37, 0xa8, 0xb3, 0x75,
|
||||
0x13, 0x47, 0x86, 0xe0, 0x81, 0x0c, 0xbd, 0xf2, 0x1c, 0x2b, 0x49, 0x6f, 0x83, 0xa1, 0x6a, 0x61,
|
||||
0xf2, 0x0b, 0x83, 0x28, 0x95, 0xe5, 0xdd, 0x22, 0x97, 0x7a, 0xf5, 0x56, 0xf3, 0xf8, 0x02, 0x82,
|
||||
0x25, 0x2a, 0x55, 0xc6, 0xde, 0xd4, 0x3b, 0x3a, 0xf8, 0xfa, 0xe9, 0x71, 0x37, 0xe8, 0x8e, 0xe7,
|
||||
0x0c, 0x95, 0x12, 0xf6, 0x16, 0xff, 0x0a, 0xa2, 0x0a, 0x37, 0x85, 0x92, 0x15, 0x96, 0xb1, 0x6f,
|
||||
0x7e, 0xc2, 0xfb, 0x9f, 0x5c, 0xb9, 0x94, 0xe8, 0x2f, 0x25, 0x7f, 0x30, 0x78, 0xbc, 0x47, 0xc5,
|
||||
0x1f, 0x01, 0x7b, 0x6d, 0xaa, 0x0a, 0x04, 0x7b, 0x4d, 0xa8, 0x31, 0x15, 0x05, 0x82, 0x35, 0x84,
|
||||
0x76, 0x66, 0x37, 0x81, 0x60, 0x3b, 0x42, 0x77, 0x66, 0x23, 0x81, 0x60, 0x77, 0xfc, 0x33, 0x98,
|
||||
0xfc, 0x54, 0xa3, 0xce, 0xb0, 0x8c, 0x03, 0xf3, 0xf2, 0x7b, 0xfd, 0xcb, 0xdf, 0xd7, 0xa8, 0x1b,
|
||||
0xd1, 0xe6, 0xa9, 0x53, 0xb3, 0x4d, 0xbb, 0x1a, 0x73, 0xa6, 0x58, 0x45, 0x9b, 0x9f, 0xd8, 0x18,
|
||||
0x9d, 0xdd, 0x84, 0xec, 0x3e, 0x46, 0xb3, 0x34, 0xf9, 0x8b, 0xd1, 0x9a, 0x6c, 0xe9, 0x83, 0xf1,
|
||||
0x99, 0x24, 0xff, 0x10, 0x42, 0x6a, 0xeb, 0xe5, 0xbd, 0xd4, 0x6e, 0x84, 0x13, 0xc2, 0x37, 0x52,
|
||||
0xf3, 0x2f, 0x61, 0x7c, 0x2f, 0x55, 0x8d, 0x0f, 0x8c, 0xb1, 0xa5, 0xbb, 0xa1, 0xbc, 0x70, 0xd7,
|
||||
0xba, 0x62, 0xfc, 0x41, 0x31, 0x4f, 0x20, 0x50, 0x72, 0x81, 0xca, 0xe9, 0xcc, 0x02, 0x5a, 0x10,
|
||||
0x75, 0xd5, 0x98, 0x5e, 0x1e, 0x64, 0xb6, 0xbd, 0xdb, 0x5b, 0xc9, 0x35, 0x3c, 0xde, 0x7b, 0xb1,
|
||||
0x7b, 0x89, 0xed, 0xbf, 0x64, 0xea, 0x70, 0x6d, 0x58, 0x40, 0x12, 0x2d, 0x51, 0xe1, 0xb2, 0xc2,
|
||||
0x95, 0x59, 0x41, 0x28, 0x3a, 0x9c, 0xfc, 0xc6, 0x7a, 0x5e, 0xf3, 0x1e, 0x89, 0x70, 0x99, 0x6f,
|
||||
0x36, 0x72, 0xbb, 0x72, 0xd4, 0x2d, 0xa4, 0xb9, 0xad, 0x16, 0x8e, 0x7a, 0xb4, 0x5a, 0x10, 0xd6,
|
||||
0x85, 0x33, 0xdc, 0x48, 0x17, 0x7c, 0x0a, 0x07, 0x1b, 0x94, 0x65, 0xad, 0x71, 0x83, 0xdb, 0xca,
|
||||
0x8d, 0x60, 0x18, 0xe2, 0x4f, 0x61, 0x52, 0xc9, 0xdb, 0x97, 0xaf, 0xb0, 0x71, 0xb3, 0x18, 0x57,
|
||||
0xf2, 0xf6, 0x1c, 0x1b, 0xfe, 0x31, 0x44, 0xeb, 0x0c, 0xd5, 0xca, 0xa4, 0xec, 0x72, 0x43, 0x13,
|
||||
0x38, 0xc7, 0x26, 0xf9, 0x9d, 0xc1, 0x78, 0x8e, 0xfa, 0x1e, 0xf5, 0x5b, 0x29, 0x7f, 0xe8, 0x7a,
|
||||
0xef, 0x7f, 0x5c, 0xef, 0x3f, 0xec, 0xfa, 0xa0, 0x77, 0xfd, 0x13, 0x08, 0xe6, 0x7a, 0x39, 0x4b,
|
||||
0x4d, 0x45, 0x9e, 0xb0, 0x80, 0x7f, 0x00, 0xe3, 0x93, 0x65, 0x95, 0xdd, 0xa3, 0xfb, 0x14, 0x38,
|
||||
0x94, 0xfc, 0xca, 0x60, 0x7c, 0x21, 0x9b, 0xbc, 0xae, 0xde, 0x50, 0xd8, 0x14, 0x0e, 0x4e, 0x8a,
|
||||
0x42, 0x65, 0x4b, 0x59, 0x65, 0xf9, 0xd6, 0x55, 0x3b, 0x0c, 0xd1, 0x8d, 0xe7, 0x83, 0xd9, 0xd9,
|
||||
0xba, 0x87, 0x21, 0xfe, 0x09, 0x04, 0x67, 0xc6, 0xd0, 0xd6, 0x9d, 0x87, 0xbd, 0x5e, 0xac, 0x8f,
|
||||
0x4d, 0x92, 0x1a, 0x3c, 0xa9, 0xab, 0x7c, 0xad, 0xf2, 0x9d, 0xe9, 0x24, 0x14, 0x1d, 0x4e, 0xfe,
|
||||
0x66, 0xe0, 0xbf, 0x2b, 0xa3, 0x3e, 0x02, 0x96, 0xb9, 0x45, 0xb2, 0xac, 0xb3, 0xed, 0x64, 0x60,
|
||||
0xdb, 0x18, 0x26, 0x8d, 0x96, 0xdb, 0x5b, 0x2c, 0xe3, 0x70, 0xea, 0x1d, 0x79, 0xa2, 0x85, 0x26,
|
||||
0x63, 0x3c, 0x52, 0xc6, 0xd1, 0xd4, 0x23, 0x05, 0x3a, 0xd8, 0x69, 0x1e, 0x7a, 0xcd, 0x27, 0x7f,
|
||||
0x32, 0x08, 0x3a, 0xe5, 0x9e, 0xed, 0x2b, 0xf7, 0xac, 0x57, 0x6e, 0x7a, 0xda, 0x2a, 0x37, 0x3d,
|
||||
0x25, 0x2c, 0x2e, 0x5b, 0xe5, 0x8a, 0x4b, 0x9a, 0xda, 0x37, 0x3a, 0xaf, 0x8b, 0xd3, 0xc6, 0x8e,
|
||||
0x37, 0x12, 0x1d, 0xa6, 0x75, 0xff, 0x70, 0x87, 0xda, 0xf5, 0x1c, 0x09, 0x87, 0x48, 0x1c, 0x17,
|
||||
0xc6, 0xd5, 0xb6, 0x4b, 0x0b, 0xf8, 0xa7, 0x10, 0x08, 0xea, 0xc2, 0xb4, 0xba, 0x37, 0x20, 0x13,
|
||||
0x16, 0x36, 0x9b, 0x3c, 0x73, 0xd7, 0x88, 0xe5, 0xba, 0x28, 0x50, 0x3b, 0x4d, 0x5b, 0x60, 0xb8,
|
||||
0xf3, 0x1d, 0xda, 0xcf, 0x91, 0x27, 0x2c, 0x48, 0x7e, 0x84, 0xe8, 0x44, 0xa1, 0xae, 0x44, 0xad,
|
||||
0xde, 0xfc, 0x88, 0x71, 0xf0, 0xbf, 0x9d, 0x7f, 0xf7, 0xa2, 0x75, 0x02, 0x9d, 0x7b, 0xfd, 0x7a,
|
||||
0xff, 0xd2, 0xef, 0xb9, 0x2c, 0xe4, 0x2c, 0x35, 0x8b, 0xf5, 0x84, 0x43, 0xc9, 0xe7, 0xe0, 0x93,
|
||||
0x4f, 0x06, 0xcc, 0xfe, 0x7f, 0x79, 0x6c, 0x31, 0x36, 0xff, 0xd6, 0xcf, 0xfe, 0x09, 0x00, 0x00,
|
||||
0xff, 0xff, 0xa7, 0xc6, 0x53, 0x22, 0xbf, 0x07, 0x00, 0x00,
|
||||
// 952 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4,
|
||||
0x13, 0x56, 0xc7, 0x76, 0x12, 0x57, 0x66, 0xe7, 0xf7, 0x53, 0x6b, 0xc5, 0x9a, 0x45, 0x42, 0xc1,
|
||||
0x02, 0x29, 0x20, 0x31, 0xa0, 0x5d, 0x21, 0x21, 0x6e, 0x99, 0x09, 0x5a, 0x85, 0x99, 0x5d, 0x86,
|
||||
0xce, 0xcc, 0x70, 0x42, 0xab, 0x4e, 0x52, 0x99, 0x58, 0xeb, 0xc4, 0xa6, 0x6d, 0x4f, 0xe2, 0xb7,
|
||||
0xe0, 0x09, 0x90, 0x90, 0x38, 0x71, 0xe0, 0xc0, 0x0b, 0xf0, 0x10, 0xbc, 0x10, 0xaa, 0xee, 0xf6,
|
||||
0x9f, 0xb0, 0xb3, 0x68, 0x4f, 0xdc, 0xfa, 0xab, 0xea, 0x7c, 0xe5, 0xfe, 0xea, 0xab, 0x52, 0xe0,
|
||||
0x38, 0xda, 0xe6, 0xa8, 0xb6, 0x32, 0x3e, 0x49, 0x55, 0x92, 0x27, 0xbc, 0x5f, 0xe1, 0xf0, 0xf7,
|
||||
0x0e, 0x74, 0x67, 0x49, 0xa1, 0x16, 0xc8, 0x8f, 0xa1, 0x33, 0x9d, 0x04, 0x6c, 0xc8, 0x46, 0x8e,
|
||||
0xe8, 0x4c, 0x27, 0x9c, 0x83, 0xfb, 0x42, 0x6e, 0x30, 0xe8, 0x0c, 0xd9, 0xc8, 0x17, 0xfa, 0x4c,
|
||||
0xb1, 0xab, 0x32, 0xc5, 0xc0, 0x31, 0x31, 0x3a, 0xf3, 0xc7, 0xd0, 0xbf, 0xce, 0x88, 0x6d, 0x83,
|
||||
0x81, 0xab, 0xe3, 0x35, 0xa6, 0xdc, 0xa5, 0xcc, 0xb2, 0x5d, 0xa2, 0x96, 0x81, 0x67, 0x72, 0x15,
|
||||
0xe6, 0xff, 0x07, 0xe7, 0x5a, 0x5c, 0x04, 0x5d, 0x1d, 0xa6, 0x23, 0x0f, 0xa0, 0x37, 0xc1, 0x95,
|
||||
0x2c, 0xe2, 0x3c, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x2a, 0x48, 0x3c, 0x57, 0x18, 0xe3, 0xad, 0x92,
|
||||
0xab, 0xa0, 0x6f, 0x78, 0x2a, 0xcc, 0x4f, 0x80, 0x4f, 0xb7, 0x19, 0x2e, 0x0a, 0x85, 0xb3, 0x57,
|
||||
0x51, 0x7a, 0x83, 0x2a, 0x5a, 0x95, 0x81, 0xaf, 0x09, 0xee, 0xc9, 0x50, 0x95, 0xe7, 0x98, 0x4b,
|
||||
0xaa, 0x0d, 0x9a, 0xaa, 0x82, 0x3c, 0x84, 0xa3, 0xd9, 0x5a, 0x2a, 0x5c, 0xce, 0x70, 0xa1, 0x30,
|
||||
0x0f, 0x06, 0x3a, 0x7d, 0x10, 0x0b, 0x7f, 0x62, 0xe0, 0x4f, 0x64, 0xb6, 0x9e, 0x27, 0x52, 0x2d,
|
||||
0xdf, 0x4a, 0xb3, 0x4f, 0xc1, 0x5b, 0x60, 0x1c, 0x67, 0x81, 0x33, 0x74, 0x46, 0x83, 0x27, 0x8f,
|
||||
0x4e, 0xea, 0x66, 0xd4, 0x3c, 0x67, 0x18, 0xc7, 0xc2, 0xdc, 0xe2, 0x9f, 0x83, 0x9f, 0xe3, 0x26,
|
||||
0x8d, 0x65, 0x8e, 0x59, 0xe0, 0xea, 0x9f, 0xf0, 0xe6, 0x27, 0x57, 0x36, 0x25, 0x9a, 0x4b, 0xe1,
|
||||
0x6f, 0x1d, 0x78, 0x70, 0x40, 0xc5, 0x8f, 0x80, 0xed, 0xf5, 0x57, 0x79, 0x82, 0xed, 0x09, 0x95,
|
||||
0xfa, 0x8b, 0x3c, 0xc1, 0x4a, 0x42, 0x3b, 0xdd, 0x3f, 0x4f, 0xb0, 0x1d, 0xa1, 0xb5, 0xee, 0x9a,
|
||||
0x27, 0xd8, 0x9a, 0x7f, 0x0c, 0xbd, 0x1f, 0x0b, 0x54, 0x11, 0x66, 0x81, 0xa7, 0x2b, 0xff, 0xaf,
|
||||
0xa9, 0xfc, 0x5d, 0x81, 0xaa, 0x14, 0x55, 0x9e, 0x5e, 0xaa, 0x3b, 0x6e, 0xda, 0xa7, 0xcf, 0x14,
|
||||
0xcb, 0xc9, 0x1d, 0x3d, 0x13, 0xa3, 0xb3, 0x55, 0xc8, 0xf4, 0x8c, 0x14, 0xfa, 0x02, 0x5c, 0xb9,
|
||||
0xc7, 0x2c, 0xf0, 0x35, 0xff, 0x07, 0x6f, 0x10, 0xe3, 0x64, 0xbc, 0xc7, 0xec, 0xeb, 0x6d, 0xae,
|
||||
0x4a, 0xa1, 0xaf, 0x3f, 0x7e, 0x06, 0x7e, 0x1d, 0x22, 0xe7, 0xbc, 0xc2, 0x52, 0x3f, 0xd0, 0x17,
|
||||
0x74, 0xe4, 0x1f, 0x82, 0x77, 0x27, 0xe3, 0xc2, 0x08, 0x3f, 0x78, 0x72, 0xdc, 0xd0, 0x8e, 0xf7,
|
||||
0x51, 0x26, 0x4c, 0xf2, 0xab, 0xce, 0x97, 0x2c, 0x7c, 0x1f, 0x5c, 0x0a, 0xf1, 0x77, 0xa0, 0x3b,
|
||||
0x4f, 0x8a, 0xed, 0x32, 0x0b, 0xd8, 0xd0, 0x19, 0x39, 0xc2, 0xa2, 0xf0, 0x4f, 0x46, 0x56, 0x33,
|
||||
0xd2, 0xb6, 0xda, 0x6b, 0x3e, 0xfe, 0x5d, 0xe8, 0x93, 0xec, 0x2f, 0xef, 0xa4, 0xb2, 0x2d, 0xee,
|
||||
0x11, 0xbe, 0x91, 0x8a, 0x7f, 0x06, 0x5d, 0x5d, 0xe4, 0x9e, 0x36, 0x57, 0x74, 0x37, 0x94, 0x17,
|
||||
0xf6, 0x5a, 0x2d, 0x96, 0xdb, 0x12, 0xeb, 0x21, 0x78, 0xb1, 0x9c, 0x63, 0x6c, 0x67, 0xc5, 0x00,
|
||||
0x32, 0x10, 0xa9, 0x5e, 0x6a, 0xad, 0xef, 0x65, 0x36, 0xbd, 0x31, 0xb7, 0xc2, 0x6b, 0x78, 0x70,
|
||||
0x50, 0xb1, 0xae, 0xc4, 0x0e, 0x2b, 0x35, 0x82, 0xf9, 0x56, 0x20, 0x1a, 0xb3, 0x0c, 0x63, 0x5c,
|
||||
0xe4, 0xb8, 0xd4, 0x16, 0xe9, 0x8b, 0x1a, 0x87, 0xbf, 0xb0, 0x86, 0x57, 0xd7, 0xa3, 0x41, 0x5a,
|
||||
0x24, 0x9b, 0x8d, 0xdc, 0x2e, 0x2d, 0x75, 0x05, 0x49, 0xb7, 0xe5, 0xdc, 0x52, 0x77, 0x96, 0x73,
|
||||
0xc2, 0x2a, 0xb5, 0x4b, 0xa3, 0xa3, 0x52, 0x3e, 0x84, 0xc1, 0x06, 0x65, 0x56, 0x28, 0xdc, 0xe0,
|
||||
0x36, 0xb7, 0x12, 0xb4, 0x43, 0xfc, 0x11, 0xf4, 0x72, 0x79, 0xfb, 0x92, 0xda, 0x6c, 0xb4, 0xe8,
|
||||
0xe6, 0xf2, 0xf6, 0x1c, 0x4b, 0xfe, 0x1e, 0xf8, 0xab, 0x08, 0xe3, 0xa5, 0x4e, 0x19, 0xf3, 0xf5,
|
||||
0x75, 0xe0, 0x1c, 0xcb, 0xf0, 0x57, 0x06, 0xdd, 0x19, 0xaa, 0x3b, 0x54, 0x6f, 0x35, 0x99, 0xed,
|
||||
0xcd, 0xe5, 0xfc, 0xcb, 0xe6, 0x72, 0xef, 0xdf, 0x5c, 0x5e, 0xb3, 0xb9, 0x1e, 0x82, 0x37, 0x53,
|
||||
0x8b, 0xe9, 0x44, 0x7f, 0x91, 0x23, 0x0c, 0x20, 0x8f, 0x8d, 0x17, 0x79, 0x74, 0x87, 0x76, 0x9d,
|
||||
0x59, 0x14, 0xfe, 0xcc, 0xa0, 0x7b, 0x21, 0xcb, 0xa4, 0xc8, 0x5f, 0x73, 0xd8, 0x10, 0x06, 0xe3,
|
||||
0x34, 0x8d, 0xa3, 0x85, 0xcc, 0xa3, 0x64, 0x6b, 0xbf, 0xb6, 0x1d, 0xa2, 0x1b, 0xcf, 0x5b, 0xda,
|
||||
0x99, 0xef, 0x6e, 0x87, 0x68, 0x18, 0xce, 0xf4, 0xc2, 0x31, 0xdb, 0xa3, 0x35, 0x0c, 0x66, 0xcf,
|
||||
0xe8, 0x24, 0x3d, 0x70, 0x5c, 0xe4, 0xc9, 0x2a, 0x4e, 0x76, 0xfa, 0x25, 0x7d, 0x51, 0xe3, 0xf0,
|
||||
0x2f, 0x06, 0xee, 0x7f, 0xb5, 0x48, 0x8e, 0x80, 0x45, 0xb6, 0x91, 0x2c, 0xaa, 0xd7, 0x4a, 0xaf,
|
||||
0xb5, 0x56, 0x02, 0xe8, 0x95, 0x4a, 0x6e, 0x6f, 0x31, 0x0b, 0xfa, 0x7a, 0x56, 0x2b, 0xa8, 0x33,
|
||||
0x7a, 0x46, 0xcc, 0x3e, 0xf1, 0x45, 0x05, 0x6b, 0xcf, 0x43, 0xe3, 0xf9, 0xf0, 0x0f, 0x06, 0x5e,
|
||||
0xed, 0xdc, 0xb3, 0x43, 0xe7, 0x9e, 0x35, 0xce, 0x9d, 0x9c, 0x56, 0xce, 0x9d, 0x9c, 0x12, 0x16,
|
||||
0x97, 0x95, 0x73, 0xc5, 0x25, 0xa9, 0xf6, 0x4c, 0x25, 0x45, 0x7a, 0x5a, 0x1a, 0x79, 0x7d, 0x51,
|
||||
0x63, 0x6a, 0xf7, 0xf7, 0x6b, 0x54, 0xf6, 0xcd, 0xbe, 0xb0, 0x88, 0xcc, 0x71, 0xa1, 0xa7, 0xda,
|
||||
0xbc, 0xd2, 0x00, 0xfe, 0x11, 0x78, 0x82, 0x5e, 0xa1, 0x9f, 0x7a, 0x20, 0x90, 0x0e, 0x0b, 0x93,
|
||||
0x0d, 0x9f, 0xda, 0x6b, 0xc4, 0x72, 0x9d, 0xa6, 0xa8, 0xac, 0xa7, 0x0d, 0xd0, 0xdc, 0xc9, 0x0e,
|
||||
0xcd, 0x3a, 0x72, 0x84, 0x01, 0xe1, 0x0f, 0xe0, 0x8f, 0x63, 0x54, 0xb9, 0x28, 0xe2, 0xd7, 0x97,
|
||||
0x18, 0x07, 0xf7, 0x9b, 0xd9, 0xb7, 0x2f, 0xaa, 0x49, 0xa0, 0x73, 0xe3, 0x5f, 0xe7, 0x1f, 0xfe,
|
||||
0x3d, 0x97, 0xa9, 0x9c, 0x4e, 0x74, 0x63, 0x1d, 0x61, 0x51, 0xf8, 0x09, 0xb8, 0x34, 0x27, 0x2d,
|
||||
0x66, 0xf7, 0x4d, 0x33, 0x36, 0xef, 0xea, 0x7f, 0x1c, 0x4f, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff,
|
||||
0x94, 0xd8, 0xce, 0x85, 0x83, 0x08, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -2,58 +2,64 @@ syntax = "proto3";
|
|||
package internal;
|
||||
|
||||
message Source {
|
||||
int64 ID = 1; // ID is the unique ID of the source
|
||||
string Name = 2; // Name is the user-defined name for the source
|
||||
string Type = 3; // Type specifies which kinds of source (enterprise vs oss)
|
||||
string Username = 4; // Username is the username to connect to the source
|
||||
int64 ID = 1; // ID is the unique ID of the source
|
||||
string Name = 2; // Name is the user-defined name for the source
|
||||
string Type = 3; // Type specifies which kinds of source (enterprise vs oss)
|
||||
string Username = 4; // Username is the username to connect to the source
|
||||
string Password = 5;
|
||||
string URL = 6; // URL are the connections to the source
|
||||
bool Default = 7; // Flags an source as the default.
|
||||
string Telegraf = 8; // Telegraf is the db telegraf is written to. By default it is "telegraf"
|
||||
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the influx server
|
||||
string URL = 6; // URL are the connections to the source
|
||||
bool Default = 7; // Flags an source as the default.
|
||||
string Telegraf = 8; // Telegraf is the db telegraf is written to. By default it is "telegraf"
|
||||
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the influx server
|
||||
string MetaURL = 10; // MetaURL is the connection URL for the meta node.
|
||||
string SharedSecret = 11; // SharedSecret signs the optional InfluxDB JWT Authorization
|
||||
}
|
||||
|
||||
message Dashboard {
|
||||
int64 ID = 1; // ID is the unique ID of the dashboard
|
||||
string Name = 2; // Name is the user-defined name of the dashboard
|
||||
repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard
|
||||
repeated Template templates = 4; // Templates replace template variables within InfluxQL
|
||||
repeated Template templates = 4; // Templates replace template variables within InfluxQL
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
message Axis {
|
||||
repeated int64 bounds = 1; // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
}
|
||||
|
||||
message Template {
|
||||
string ID = 1; // ID is the unique ID associated with this template
|
||||
string temp_var = 2;
|
||||
repeated TemplateValue values = 3;
|
||||
string type = 4; // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
|
||||
string label = 5; // Label is a user-facing description of the Template
|
||||
TemplateQuery query = 6; // Query is used to generate the choices for a template
|
||||
string ID = 1; // ID is the unique ID associated with this template
|
||||
string temp_var = 2;
|
||||
repeated TemplateValue values = 3;
|
||||
string type = 4; // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
|
||||
string label = 5; // Label is a user-facing description of the Template
|
||||
TemplateQuery query = 6; // Query is used to generate the choices for a 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 {
|
||||
|
@ -62,54 +68,59 @@ message Server {
|
|||
string Username = 3; // Username is the username to connect to the server
|
||||
string Password = 4;
|
||||
string URL = 5; // URL is the path to the server
|
||||
int64 SrcID = 6; // SrcID is the ID of the data source
|
||||
int64 SrcID = 6; // SrcID is the ID of the data source
|
||||
bool Active = 7; // is this the currently active server for the source
|
||||
}
|
||||
|
||||
message Layout {
|
||||
string ID = 1; // ID is the unique ID of the layout.
|
||||
string Application = 2; // Application is the user facing name of this Layout.
|
||||
string Measurement = 3; // Measurement is the descriptive name of the time series data.
|
||||
repeated Cell Cells = 4; // Cells are the individual visualization elements.
|
||||
bool Autoflow = 5; // Autoflow indicates whether the frontend should layout the cells automatically.
|
||||
string ID = 1; // ID is the unique ID of the layout.
|
||||
string Application = 2; // Application is the user facing name of this Layout.
|
||||
string Measurement = 3; // Measurement is the descriptive name of the time series data.
|
||||
repeated Cell Cells = 4; // Cells are the individual visualization elements.
|
||||
bool Autoflow = 5; // Autoflow indicates whether the frontend should layout the cells automatically.
|
||||
}
|
||||
|
||||
message Cell {
|
||||
int32 x = 1; // X-coordinate of Cell in the Layout
|
||||
int32 y = 2; // Y-coordinate of Cell in the Layout
|
||||
int32 w = 3; // Width of Cell in the Layout
|
||||
int32 h = 4; // Height of Cell in the Layout
|
||||
repeated Query queries = 5; // Time-series data queries for Cell.
|
||||
string i = 6; // Unique identifier for the cell
|
||||
string name = 7; // User-facing name for this cell
|
||||
repeated int64 yranges = 8; // Limits of the y-axes
|
||||
repeated string ylabels = 9; // Labels of the y-axes
|
||||
string type = 10; // Cell visualization type
|
||||
int32 x = 1; // X-coordinate of Cell in the Layout
|
||||
int32 y = 2; // Y-coordinate of Cell in the Layout
|
||||
int32 w = 3; // Width of Cell in the Layout
|
||||
int32 h = 4; // Height of Cell in the Layout
|
||||
repeated Query queries = 5; // Time-series data queries for Cell.
|
||||
string i = 6; // Unique identifier for the cell
|
||||
string name = 7; // User-facing name for this cell
|
||||
repeated int64 yranges = 8; // Limits of the y-axes
|
||||
repeated string ylabels = 9; // Labels of the y-axes
|
||||
string type = 10; // Cell visualization type
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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
|
||||
// appropriate tabbing and whitespace management to edit this file
|
||||
//
|
||||
// vim: ai:ts=4:noet:sts=4
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt/internal"
|
||||
)
|
||||
|
@ -40,6 +41,38 @@ func TestMarshalSource(t *testing.T) {
|
|||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
}
|
||||
func TestMarshalSourceWithSecret(t *testing.T) {
|
||||
v := chronograf.Source{
|
||||
ID: 12,
|
||||
Name: "Fountain of Truth",
|
||||
Type: "influx",
|
||||
Username: "docbrown",
|
||||
SharedSecret: "hunter2s",
|
||||
URL: "http://twin-pines.mall.io:8086",
|
||||
MetaURL: "http://twin-pines.meta.io:8086",
|
||||
Default: true,
|
||||
Telegraf: "telegraf",
|
||||
}
|
||||
|
||||
var vv chronograf.Source
|
||||
if buf, err := internal.MarshalSource(v); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := internal.UnmarshalSource(buf, &vv); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(v, vv) {
|
||||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
|
||||
// Test if the new insecureskipverify works
|
||||
v.InsecureSkipVerify = true
|
||||
if buf, err := internal.MarshalSource(v); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := internal.UnmarshalSource(buf, &vv); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(v, vv) {
|
||||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalServer(t *testing.T) {
|
||||
v := chronograf.Server{
|
||||
|
@ -104,3 +137,45 @@ func TestMarshalLayout(t *testing.T) {
|
|||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, layout)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MarshalDashboard(t *testing.T) {
|
||||
dashboard := chronograf.Dashboard{
|
||||
ID: 1,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Super awesome query",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "select * from cpu",
|
||||
Label: "CPU Utilization",
|
||||
Range: &chronograf.Range{
|
||||
Upper: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
Templates: []chronograf.Template{},
|
||||
Name: "Dashboard",
|
||||
}
|
||||
|
||||
var actual chronograf.Dashboard
|
||||
if buf, err := internal.MarshalDashboard(dashboard); err != nil {
|
||||
t.Fatal("Error marshaling dashboard: err", err)
|
||||
} else if err := internal.UnmarshalDashboard(buf, &actual); err != nil {
|
||||
t.Fatal("Error unmarshaling dashboard: err:", err)
|
||||
} else if !cmp.Equal(dashboard, actual) {
|
||||
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(dashboard, actual))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ const (
|
|||
ErrAlertNotFound = Error("alert not found")
|
||||
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'")
|
||||
)
|
||||
|
||||
// Error is a domain error encountered while processing chronograf requests
|
||||
|
@ -170,7 +171,7 @@ func (t BasicTemplateVar) String() string {
|
|||
switch t.Values[0].Type {
|
||||
case "tagKey", "fieldKey", "measurement", "database":
|
||||
return `"` + t.Values[0].Value + `"`
|
||||
case "tagValue":
|
||||
case "tagValue", "timeStamp":
|
||||
return `'` + t.Values[0].Value + `'`
|
||||
case "csv", "constant":
|
||||
return t.Values[0].Value
|
||||
|
@ -346,6 +347,7 @@ type Source struct {
|
|||
Type string `json:"type,omitempty"` // Type specifies which kinds of source (enterprise vs oss)
|
||||
Username string `json:"username,omitempty"` // Username is the username to connect to the source
|
||||
Password string `json:"password,omitempty"` // Password is in CLEARTEXT
|
||||
SharedSecret string `json:"sharedSecret,omitempty"` // ShareSecret is the optional signing secret for Influx JWT authorization
|
||||
URL string `json:"url"` // URL are the connections to the source
|
||||
MetaURL string `json:"metaUrl,omitempty"` // MetaURL is the url for the meta node
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the source is accepted.
|
||||
|
@ -565,6 +567,11 @@ type Dashboard struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Axis represents the visible extents of a visualization
|
||||
type Axis struct {
|
||||
Bounds [2]int64 `json:"bounds"` // bounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
}
|
||||
|
||||
// DashboardCell holds visual and query information for a cell
|
||||
type DashboardCell struct {
|
||||
ID string `json:"i"`
|
||||
|
@ -574,6 +581,7 @@ type DashboardCell struct {
|
|||
H int32 `json:"h"`
|
||||
Name string `json:"name"`
|
||||
Queries []DashboardQuery `json:"queries"`
|
||||
Axes map[string]Axis `json:"axes"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
|
@ -625,44 +633,3 @@ type LayoutStore interface {
|
|||
// Update the dashboard in the store.
|
||||
Update(context.Context, Layout) error
|
||||
}
|
||||
|
||||
// SourceAndKapacitor is used to parse any NewSources server flag arguments
|
||||
type SourceAndKapacitor struct {
|
||||
Source Source `json:"influxdb"`
|
||||
Kapacitor Server `json:"kapacitor"`
|
||||
}
|
||||
|
||||
// NewSources adds sources to BoltDb idempotently by name, as well as respective kapacitors
|
||||
func NewSources(ctx context.Context, sourcesStore SourcesStore, serversStore ServersStore, srcsKaps []SourceAndKapacitor, logger Logger) error {
|
||||
srcs, err := sourcesStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SourceLoop:
|
||||
for _, srcKap := range srcsKaps {
|
||||
for _, src := range srcs {
|
||||
// If source already exists, do nothing
|
||||
if src.Name == srcKap.Source.Name {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", src.Name).
|
||||
Info("Source already exists")
|
||||
continue SourceLoop
|
||||
}
|
||||
}
|
||||
|
||||
src, err := sourcesStore.Add(ctx, srcKap.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcKap.Kapacitor.SrcID = src.ID
|
||||
_, err = serversStore.Add(ctx, srcKap.Kapacitor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
package chronograf_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func Test_NewSources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srcsKaps := []chronograf.SourceAndKapacitor{
|
||||
{
|
||||
Source: chronograf.Source{
|
||||
Default: true,
|
||||
InsecureSkipVerify: false,
|
||||
MetaURL: "http://metaurl.com",
|
||||
Name: "Influx 1",
|
||||
Password: "pass1",
|
||||
Telegraf: "telegraf",
|
||||
URL: "http://localhost:8086",
|
||||
Username: "user1",
|
||||
},
|
||||
Kapacitor: chronograf.Server{
|
||||
Active: true,
|
||||
Name: "Kapa 1",
|
||||
URL: "http://localhost:9092",
|
||||
},
|
||||
},
|
||||
}
|
||||
saboteurSrcsKaps := []chronograf.SourceAndKapacitor{
|
||||
{
|
||||
Source: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
Kapacitor: chronograf.Server{
|
||||
Name: "Kapa Aspiring Saboteur",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
srcs := []chronograf.Source{}
|
||||
srcsStore := mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return srcs, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcs = append(srcs, src)
|
||||
return src, nil
|
||||
},
|
||||
}
|
||||
srvs := []chronograf.Server{}
|
||||
srvsStore := mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvs = append(srvs, srv)
|
||||
return srv, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := chronograf.NewSources(ctx, &srcsStore, &srvsStore, srcsKaps, &mocks.TestLogger{})
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when creating New Sources. Error:", err)
|
||||
}
|
||||
if len(srcs) != 1 {
|
||||
t.Error("Expected one source in sourcesStore")
|
||||
}
|
||||
if len(srvs) != 1 {
|
||||
t.Error("Expected one source in serversStore")
|
||||
}
|
||||
|
||||
err = chronograf.NewSources(ctx, &srcsStore, &srvsStore, saboteurSrcsKaps, &mocks.TestLogger{})
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when creating New Sources. Error:", err)
|
||||
}
|
||||
if len(srcs) != 1 {
|
||||
t.Error("Expected one source in sourcesStore")
|
||||
}
|
||||
if len(srvs) != 1 {
|
||||
t.Error("Expected one source in serversStore")
|
||||
}
|
||||
if !reflect.DeepEqual(srcs[0], srcsKaps[0].Source) {
|
||||
t.Error("Expected source in sourceStore to remain unchanged")
|
||||
}
|
||||
}
|
|
@ -120,13 +120,17 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
|||
|
||||
c.dataNodes = ring.New(len(cluster.DataNodes))
|
||||
for _, dn := range cluster.DataNodes {
|
||||
cl, err := influx.NewClient(dn.HTTPAddr, c.Logger)
|
||||
if err != nil {
|
||||
continue
|
||||
} else {
|
||||
c.dataNodes.Value = cl
|
||||
c.dataNodes = c.dataNodes.Next()
|
||||
cl := &influx.Client{
|
||||
Logger: c.Logger,
|
||||
}
|
||||
dataSrc := &chronograf.Source{}
|
||||
*dataSrc = *src
|
||||
dataSrc.URL = dn.HTTPAddr
|
||||
if err := cl.Connect(ctx, dataSrc); err != nil {
|
||||
continue
|
||||
}
|
||||
c.dataNodes.Value = cl
|
||||
c.dataNodes = c.dataNodes.Next()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Authorizer adds optional authorization header to request
|
||||
type Authorizer interface {
|
||||
// Set may manipulate the request by adding the Authorization header
|
||||
Set(req *http.Request) error
|
||||
}
|
||||
|
||||
// NoAuthorization does not add any authorization headers
|
||||
type NoAuthorization struct{}
|
||||
|
||||
// Set does not add authorization
|
||||
func (n *NoAuthorization) Set(req *http.Request) error { return nil }
|
||||
|
||||
// DefaultAuthorization creates either a shared JWT builder, basic auth or Noop
|
||||
func DefaultAuthorization(src *chronograf.Source) Authorizer {
|
||||
// Optionally, add the shared secret JWT token creation
|
||||
if src.Username != "" && src.SharedSecret != "" {
|
||||
return &BearerJWT{
|
||||
Username: src.Username,
|
||||
SharedSecret: src.SharedSecret,
|
||||
}
|
||||
} else if src.Username != "" && src.Password != "" {
|
||||
return &BasicAuth{
|
||||
Username: src.Username,
|
||||
Password: src.Password,
|
||||
}
|
||||
}
|
||||
return &NoAuthorization{}
|
||||
}
|
||||
|
||||
// BasicAuth adds Authorization: Basic to the request header
|
||||
type BasicAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Set adds the basic auth headers to the request
|
||||
func (b *BasicAuth) Set(r *http.Request) error {
|
||||
r.SetBasicAuth(b.Username, b.Password)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BearerJWT is the default Bearer for InfluxDB
|
||||
type BearerJWT struct {
|
||||
Username string
|
||||
SharedSecret string
|
||||
}
|
||||
|
||||
// Set adds an Authorization Bearer to the request if has a shared secret
|
||||
func (b *BearerJWT) Set(r *http.Request) error {
|
||||
if b.SharedSecret != "" && b.Username != "" {
|
||||
token, err := b.Token(b.Username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create token")
|
||||
}
|
||||
r.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Now returns the current time
|
||||
type Now func() time.Time
|
||||
|
||||
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
|
||||
func JWT(username, sharedSecret string, now Now) (string, error) {
|
||||
token := &jwt.Token{
|
||||
Header: map[string]interface{}{
|
||||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS512.Alg(),
|
||||
},
|
||||
Claims: jwt.MapClaims{
|
||||
"username": username,
|
||||
"exp": now().Add(time.Minute).Unix(),
|
||||
},
|
||||
Method: jwt.SigningMethodHS512,
|
||||
}
|
||||
return token.SignedString([]byte(sharedSecret))
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJWT(t *testing.T) {
|
||||
type args struct {
|
||||
username string
|
||||
sharedSecret string
|
||||
now Now
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "",
|
||||
args: args{
|
||||
username: "AzureDiamond",
|
||||
sharedSecret: "hunter2",
|
||||
now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
},
|
||||
want: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjYwLCJ1c2VybmFtZSI6IkF6dXJlRGlhbW9uZCJ9.kUWGwcpCPwV7MEk7luO1rt8036LyvG4bRL_CfseQGmz4b0S34gATx30g4xvqVAV6bwwYE0YU3P8FjG8ij4kc5g",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := JWT(tt.args.username, tt.args.sharedSecret, tt.args.now)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWT() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("JWT() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -28,25 +28,9 @@ var (
|
|||
// Client is a device for retrieving time series data from an InfluxDB instance
|
||||
type Client struct {
|
||||
URL *url.URL
|
||||
Authorizer Authorizer
|
||||
InsecureSkipVerify bool
|
||||
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewClient initializes an HTTP Client for InfluxDB. UDP, although supported
|
||||
// for querying InfluxDB, is not supported here to remove the need to
|
||||
// explicitly Close the client.
|
||||
func NewClient(host string, lg chronograf.Logger) (*Client, error) {
|
||||
l := lg.WithField("host", host)
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
l.Error("Error initialize influx client: err:", err)
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
URL: u,
|
||||
Logger: l,
|
||||
}, nil
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// Response is a partial JSON decoded InfluxQL response used
|
||||
|
@ -88,6 +72,13 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
|||
params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
if c.Authorizer != nil {
|
||||
if err := c.Authorizer.Set(req); err != nil {
|
||||
logs.Error("Error setting authorization header ", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hc := &http.Client{}
|
||||
if c.InsecureSkipVerify {
|
||||
hc.Transport = skipVerifyTransport
|
||||
|
@ -157,17 +148,18 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp
|
|||
}
|
||||
}
|
||||
|
||||
// Connect caches the URL for the data source
|
||||
// Connect caches the URL and optional Bearer Authorization for the data source
|
||||
func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
||||
u, err := url.Parse(src.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.User = url.UserPassword(src.Username, src.Password)
|
||||
c.Authorizer = DefaultAuthorization(src)
|
||||
// Only allow acceptance of all certs if the scheme is https AND the user opted into to the setting.
|
||||
if u.Scheme == "https" && src.InsecureSkipVerify {
|
||||
c.InsecureSkipVerify = src.InsecureSkipVerify
|
||||
}
|
||||
|
||||
c.URL = u
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,16 +2,34 @@ package influx_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gojwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
)
|
||||
|
||||
// NewClient initializes an HTTP Client for InfluxDB.
|
||||
func NewClient(host string, lg chronograf.Logger) (*influx.Client, error) {
|
||||
l := lg.WithField("host", host)
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
l.Error("Error initialize influx client: err:", err)
|
||||
return nil, err
|
||||
}
|
||||
return &influx.Client{
|
||||
URL: u,
|
||||
Logger: l,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
called := false
|
||||
|
@ -26,7 +44,7 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
|||
defer ts.Close()
|
||||
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -44,6 +62,140 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type MockAuthorization struct {
|
||||
Bearer string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockAuthorization) Set(req *http.Request) error {
|
||||
return m.Error
|
||||
}
|
||||
func Test_Influx_AuthorizationBearer(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{}`))
|
||||
auth := r.Header.Get("Authorization")
|
||||
tokenString := strings.Split(auth, " ")[1]
|
||||
token, err := gojwt.Parse(tokenString, func(token *gojwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte("42"), nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Invalid token %v", err)
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(gojwt.MapClaims); ok && token.Valid {
|
||||
got := claims["username"]
|
||||
want := "AzureDiamond"
|
||||
if got != want {
|
||||
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("Invalid token %v", token)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
src := &chronograf.Source{
|
||||
Username: "AzureDiamond",
|
||||
URL: ts.URL,
|
||||
SharedSecret: "42",
|
||||
}
|
||||
series := &influx.Client{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
series.Connect(context.Background(), src)
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err := series.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error but was", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_AuthorizationBearerCtx(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{}`))
|
||||
got := r.Header.Get("Authorization")
|
||||
if got == "" {
|
||||
t.Error("Test_Influx_AuthorizationBearerCtx got empty string")
|
||||
}
|
||||
incomingToken := strings.Split(got, " ")[1]
|
||||
|
||||
alg := func(token *gojwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte("hunter2"), nil
|
||||
}
|
||||
claims := &gojwt.MapClaims{}
|
||||
token, err := gojwt.ParseWithClaims(string(incomingToken), claims, alg)
|
||||
if err != nil {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx unexpected claims error %v", err)
|
||||
}
|
||||
if !token.Valid {
|
||||
t.Error("Test_Influx_AuthorizationBearerCtx unexpected valid claim")
|
||||
}
|
||||
if err := claims.Valid(); err != nil {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx not expires already %v", err)
|
||||
}
|
||||
user := (*claims)["username"].(string)
|
||||
if user != "AzureDiamond" {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx expected username AzureDiamond but got %s", user)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
series := &influx.Client{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
err := series.Connect(context.Background(), &chronograf.Source{
|
||||
Username: "AzureDiamond",
|
||||
SharedSecret: "hunter2",
|
||||
URL: ts.URL,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err = series.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error but was", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
bearer := &MockAuthorization{
|
||||
Error: fmt.Errorf("cracked1337"),
|
||||
}
|
||||
|
||||
u, _ := url.Parse("http://haxored.net")
|
||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
||||
series := &influx.Client{
|
||||
URL: u,
|
||||
Authorizer: bearer,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err := series.Query(context.Background(), query)
|
||||
if err == nil {
|
||||
t.Fatal("Test_Influx_AuthorizationBearerFailure Expected error but received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_HTTPS_Failure(t *testing.T) {
|
||||
called := false
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
|
@ -53,7 +205,7 @@ func Test_Influx_HTTPS_Failure(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -97,7 +249,7 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -166,7 +318,7 @@ func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
|||
ts.Close()
|
||||
}()
|
||||
|
||||
series, _ := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, _ := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
errs := make(chan (error))
|
||||
|
@ -209,7 +361,7 @@ func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_Influx_RejectsInvalidHosts(t *testing.T) {
|
||||
_, err := influx.NewClient(":", log.New(log.DebugLevel))
|
||||
_, err := NewClient(":", log.New(log.DebugLevel))
|
||||
if err == nil {
|
||||
t.Fatal("Expected err but was nil")
|
||||
}
|
||||
|
@ -221,7 +373,7 @@ func Test_Influx_ReportsInfluxErrs(t *testing.T) {
|
|||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cl, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
cl, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Encountered unexpected error while initializing influx client: err:", err)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ func kapaHandler(handler string) (string, error) {
|
|||
return "email", nil
|
||||
case "http":
|
||||
return "post", nil
|
||||
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log":
|
||||
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log", "pushover":
|
||||
return handler, nil
|
||||
default:
|
||||
return "", fmt.Errorf("Unsupported alert handler %s", handler)
|
||||
|
|
|
@ -169,7 +169,51 @@ func TestAlertServices(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
want: `alert()
|
||||
.post()
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Test post with headers",
|
||||
rule: chronograf.AlertRule{
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "post",
|
||||
Args: []string{"http://myaddress"},
|
||||
Properties: []chronograf.KapacitorProperty{
|
||||
{
|
||||
Name: "header",
|
||||
Args: []string{"key", "value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `alert()
|
||||
.post('http://myaddress')
|
||||
.header('key', 'value')
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Test post with headers",
|
||||
rule: chronograf.AlertRule{
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "post",
|
||||
Args: []string{"http://myaddress"},
|
||||
Properties: []chronograf.KapacitorProperty{
|
||||
{
|
||||
Name: "endpoint",
|
||||
Args: []string{"myendpoint"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `alert()
|
||||
.post('http://myaddress')
|
||||
.endpoint('myendpoint')
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
100
kapacitor/ast.go
100
kapacitor/ast.go
|
@ -412,7 +412,6 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
|
|||
rule.Query.RetentionPolicy = commonVars.RP
|
||||
rule.Query.Measurement = commonVars.Measurement
|
||||
rule.Query.GroupBy.Tags = commonVars.GroupBy
|
||||
|
||||
if commonVars.Filter.Operator == "==" {
|
||||
rule.Query.AreTagsAccepted = true
|
||||
}
|
||||
|
@ -492,6 +491,7 @@ func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
|
|||
extractSlack(t, rule)
|
||||
extractTalk(t, rule)
|
||||
extractTelegram(t, rule)
|
||||
extractPushover(t, rule)
|
||||
extractTCP(t, rule)
|
||||
extractLog(t, rule)
|
||||
extractExec(t, rule)
|
||||
|
@ -501,7 +501,7 @@ func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.HipChatHandlers == nil {
|
||||
if len(node.HipChatHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "hipchat")
|
||||
|
@ -527,7 +527,7 @@ func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.OpsGenieHandlers == nil {
|
||||
if len(node.OpsGenieHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "opsgenie")
|
||||
|
@ -553,13 +553,13 @@ func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.PagerDutyHandlers == nil {
|
||||
if len(node.PagerDutyHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "pagerduty")
|
||||
p := node.PagerDutyHandlers[0]
|
||||
alert := chronograf.KapacitorNode{
|
||||
Name: "paperduty",
|
||||
Name: "pagerduty",
|
||||
}
|
||||
|
||||
if p.ServiceKey != "" {
|
||||
|
@ -572,7 +572,7 @@ func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.VictorOpsHandlers == nil {
|
||||
if len(node.VictorOpsHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "victorops")
|
||||
|
@ -591,7 +591,7 @@ func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.EmailHandlers == nil {
|
||||
if len(node.EmailHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "smtp")
|
||||
|
@ -607,11 +607,11 @@ func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.PostHandlers == nil {
|
||||
if len(node.HTTPPostHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "http")
|
||||
p := node.PostHandlers[0]
|
||||
p := node.HTTPPostHandlers[0]
|
||||
alert := chronograf.KapacitorNode{
|
||||
Name: "http",
|
||||
}
|
||||
|
@ -620,11 +620,27 @@ func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
alert.Args = []string{p.URL}
|
||||
}
|
||||
|
||||
if p.Endpoint != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "endpoint",
|
||||
Args: []string{p.Endpoint},
|
||||
})
|
||||
}
|
||||
|
||||
if len(p.Headers) > 0 {
|
||||
for k, v := range p.Headers {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "header",
|
||||
Args: []string{k, v},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.AlertaHandlers == nil {
|
||||
if len(node.AlertaHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "alerta")
|
||||
|
@ -693,7 +709,7 @@ func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.SensuHandlers == nil {
|
||||
if len(node.SensuHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "sensu")
|
||||
|
@ -705,7 +721,7 @@ func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.SlackHandlers == nil {
|
||||
if len(node.SlackHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "slack")
|
||||
|
@ -736,8 +752,9 @@ func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.TalkHandlers == nil {
|
||||
if len(node.TalkHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "talk")
|
||||
|
@ -747,8 +764,9 @@ func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.TelegramHandlers == nil {
|
||||
if len(node.TelegramHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "telegram")
|
||||
|
@ -786,7 +804,7 @@ func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.TcpHandlers == nil {
|
||||
if len(node.TcpHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "tcp")
|
||||
|
@ -803,7 +821,7 @@ func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.LogHandlers == nil {
|
||||
if len(node.LogHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "log")
|
||||
|
@ -820,7 +838,7 @@ func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
}
|
||||
|
||||
func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if node.ExecHandlers == nil {
|
||||
if len(node.ExecHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "exec")
|
||||
|
@ -835,3 +853,51 @@ func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
|||
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
||||
func extractPushover(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
|
||||
if len(node.PushoverHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
rule.Alerts = append(rule.Alerts, "pushover")
|
||||
a := node.PushoverHandlers[0]
|
||||
alert := chronograf.KapacitorNode{
|
||||
Name: "pushover",
|
||||
}
|
||||
|
||||
if a.Device != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "device",
|
||||
Args: []string{a.Device},
|
||||
})
|
||||
}
|
||||
|
||||
if a.Title != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "title",
|
||||
Args: []string{a.Title},
|
||||
})
|
||||
}
|
||||
|
||||
if a.URL != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "URL",
|
||||
Args: []string{a.URL},
|
||||
})
|
||||
}
|
||||
|
||||
if a.URLTitle != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "URLTitle",
|
||||
Args: []string{a.URLTitle},
|
||||
})
|
||||
}
|
||||
|
||||
if a.Sound != "" {
|
||||
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
|
||||
Name: "sound",
|
||||
Args: []string{a.Sound},
|
||||
})
|
||||
}
|
||||
|
||||
rule.AlertNodes = append(rule.AlertNodes, alert)
|
||||
}
|
||||
|
|
|
@ -60,12 +60,15 @@ func TestReverse(t *testing.T) {
|
|||
.victorOps()
|
||||
.email('howdy@howdy.com')
|
||||
.log('/tmp/alerts.log')
|
||||
.post('http://backin.tm')
|
||||
.endpoint('myendpoint')
|
||||
.header('key', 'value')
|
||||
`),
|
||||
|
||||
want: chronograf.AlertRule{
|
||||
Name: "name",
|
||||
Trigger: "threshold",
|
||||
Alerts: []string{"victorops", "smtp", "slack", "log"},
|
||||
Alerts: []string{"victorops", "smtp", "http", "slack", "log"},
|
||||
AlertNodes: []chronograf.KapacitorNode{
|
||||
{
|
||||
Name: "victorops",
|
||||
|
@ -74,6 +77,20 @@ func TestReverse(t *testing.T) {
|
|||
Name: "smtp",
|
||||
Args: []string{"howdy@howdy.com"},
|
||||
},
|
||||
{
|
||||
Name: "http",
|
||||
Args: []string{"http://backin.tm"},
|
||||
Properties: []chronograf.KapacitorProperty{
|
||||
{
|
||||
Name: "endpoint",
|
||||
Args: []string{"myendpoint"},
|
||||
},
|
||||
{
|
||||
Name: "header",
|
||||
Args: []string{"key", "value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "slack",
|
||||
},
|
||||
|
|
|
@ -43,9 +43,23 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
|
|||
return cells
|
||||
}
|
||||
|
||||
// ValidDashboardCellRequest verifies that the dashboard cells have a query
|
||||
// ValidDashboardCellRequest verifies that the dashboard cells have a query and
|
||||
// have the correct axes specified
|
||||
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
||||
CorrectWidthHeight(c)
|
||||
return HasCorrectAxes(c)
|
||||
}
|
||||
|
||||
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
|
||||
func HasCorrectAxes(c *chronograf.DashboardCell) error {
|
||||
for axis, _ := range c.Axes {
|
||||
switch axis {
|
||||
case "x", "y", "y2":
|
||||
// no-op
|
||||
default:
|
||||
return chronograf.ErrInvalidAxis
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package server_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/server"
|
||||
)
|
||||
|
||||
func Test_Cells_CorrectAxis(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
axisTests := []struct {
|
||||
name string
|
||||
cell *chronograf.DashboardCell
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
"correct axes",
|
||||
&chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid axes present",
|
||||
&chronograf.DashboardCell{
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"axis of evil": chronograf.Axis{
|
||||
Bounds: [2]int64{666, 666},
|
||||
},
|
||||
"axis of awesome": chronograf.Axis{
|
||||
Bounds: [2]int64{1337, 31337},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range axisTests {
|
||||
t.Run(test.name, func(tt *testing.T) {
|
||||
if err := server.HasCorrectAxes(test.cell); err != nil && !test.shouldFail {
|
||||
t.Errorf("%q: Unexpected error: err: %s", test.name, err)
|
||||
} else if err == nil && test.shouldFail {
|
||||
t.Errorf("%q: Expected error and received none", test.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
|
@ -219,6 +220,14 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'",
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{2, 95},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "b",
|
||||
|
@ -257,6 +266,14 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: [2]int64{0, 100},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: [2]int64{2, 95},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dashboardCellResponse{
|
||||
|
@ -301,8 +318,8 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. newDashboardResponse() = \n%#v\n\n, want\n\n%#v", tt.name, got, tt.want)
|
||||
if got := newDashboardResponse(tt.d); !cmp.Equal(got, tt.want) {
|
||||
t.Errorf("%q. newDashboardResponse() = diff:\n%s", tt.name, cmp.Diff(got, tt.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
||||
// ValidInfluxRequest checks if queries specify a command.
|
||||
|
@ -106,10 +107,9 @@ func (h *Service) Write(w http.ResponseWriter, r *http.Request) {
|
|||
req.Host = u.Host
|
||||
req.URL = u
|
||||
// Because we are acting as a proxy, influxdb needs to have the
|
||||
// basic auth information set as a header directly
|
||||
if src.Username != "" && src.Password != "" {
|
||||
req.SetBasicAuth(src.Username, src.Password)
|
||||
}
|
||||
// basic auth or bearer token information set as a header directly
|
||||
auth := influx.DefaultAuthorization(&src)
|
||||
auth.Set(req)
|
||||
}
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
|
|
|
@ -3,7 +3,6 @@ package server
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
@ -51,10 +50,10 @@ type Server struct {
|
|||
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
|
||||
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
|
||||
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDb source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES"`
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"hunter2\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
|
||||
|
||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
|
||||
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
|
||||
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
|
||||
|
@ -301,8 +300,13 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
|
||||
|
||||
go processNewSources(ctx, service, s.NewSources, logger)
|
||||
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("new-sources", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
basepath = s.Basepath
|
||||
if basepath != "" && s.PrefixRoutes == false {
|
||||
|
@ -437,33 +441,6 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
}
|
||||
}
|
||||
|
||||
// processNewSources parses and persists new sources passed in via server flag
|
||||
func processNewSources(ctx context.Context, service Service, newSources string, logger chronograf.Logger) error {
|
||||
if newSources == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var srcsKaps []chronograf.SourceAndKapacitor
|
||||
// On JSON unmarshal error, continue server process without new source and write error to log
|
||||
if err := json.Unmarshal([]byte(newSources), &srcsKaps); err != nil {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
}
|
||||
|
||||
// Add any new sources and kapacitors as specified via server flag
|
||||
if err := chronograf.NewSources(ctx, service.SourcesStore, service.ServersStore, srcsKaps, logger); err != nil {
|
||||
// Continue with server run even if adding NewSources fails
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportUsageStats starts periodic server reporting.
|
||||
func reportUsageStats(bi BuildInfo, logger chronograf.Logger) {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
|
|
@ -43,18 +43,15 @@ type InfluxClient struct{}
|
|||
|
||||
// New creates a client to connect to OSS or enterprise
|
||||
func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chronograf.TimeSeries, error) {
|
||||
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
|
||||
dataNode := &influx.Client{
|
||||
Logger: logger,
|
||||
}
|
||||
if err := dataNode.Connect(context.TODO(), &src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tls := strings.Contains(src.MetaURL, "https")
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, src.Username, src.Password, tls, dataNode)
|
||||
}
|
||||
return &influx.Client{
|
||||
client := &influx.Client{
|
||||
Logger: logger,
|
||||
}, nil
|
||||
}
|
||||
if err := client.Connect(context.TODO(), &src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 client, nil
|
||||
}
|
||||
|
|
|
@ -34,8 +34,9 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
|
|||
src.Telegraf = "telegraf"
|
||||
}
|
||||
|
||||
// Omit the password on response
|
||||
// Omit the password and shared secret on response
|
||||
src.Password = ""
|
||||
src.SharedSecret = ""
|
||||
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
res := sourceResponse{
|
||||
|
@ -99,6 +100,7 @@ func (h *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string,
|
|||
cli := &influx.Client{
|
||||
Logger: h.Logger,
|
||||
}
|
||||
|
||||
if err := cli.Connect(ctx, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -293,3 +295,69 @@ func ValidSourceRequest(s chronograf.Source) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleNewSources parses and persists new sources passed in via server flag
|
||||
func (h *Service) HandleNewSources(ctx context.Context, input string) error {
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var srcsKaps []struct {
|
||||
Source chronograf.Source `json:"influxdb"`
|
||||
Kapacitor chronograf.Server `json:"kapacitor"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(input), &srcsKaps); err != nil {
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sk := range srcsKaps {
|
||||
if err := ValidSourceRequest(sk.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add any new sources and kapacitors as specified via server flag
|
||||
if err := h.newSourceKapacitor(ctx, sk.Source, sk.Kapacitor); err != nil {
|
||||
// Continue with server run even if adding NewSource fails
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSource", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSourceKapacitor adds sources to BoltDB idempotently by name, as well as respective kapacitors
|
||||
func (h *Service) newSourceKapacitor(ctx context.Context, src chronograf.Source, kapa chronograf.Server) error {
|
||||
srcs, err := h.SourcesStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range srcs {
|
||||
// If source already exists, do nothing
|
||||
if s.Name == src.Name {
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSource", s.Name).
|
||||
Info("Source already exists")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
src, err = h.SourcesStore.Add(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kapa.SrcID = src.ID
|
||||
if _, err := h.ServersStore.Add(ctx, kapa); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func Test_newSourceResponse(t *testing.T) {
|
||||
|
@ -66,3 +69,180 @@ func Test_newSourceResponse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_newSourceKapacitor(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
ServersStore chronograf.ServersStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
src chronograf.Source
|
||||
kapa chronograf.Server
|
||||
}
|
||||
srcCount := 0
|
||||
srvCount := 0
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantSrc int
|
||||
wantSrv int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Add when no existing sources",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return srv, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 1,
|
||||
wantSrv: 1,
|
||||
},
|
||||
{
|
||||
name: "Should not add if existing source",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{
|
||||
{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return srv, nil
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 0,
|
||||
wantSrv: 0,
|
||||
},
|
||||
{
|
||||
name: "Error if All returns error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return nil, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error if Add returns error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
return chronograf.Source{}, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error if kapa add is error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return chronograf.Server{}, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 1,
|
||||
wantSrv: 1,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srcCount = 0
|
||||
srvCount = 0
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
ServersStore: tt.fields.ServersStore,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
if err := h.newSourceKapacitor(tt.args.ctx, tt.args.src, tt.args.kapa); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Service.newSourceKapacitor() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantSrc != srcCount {
|
||||
t.Errorf("Service.newSourceKapacitor() count = %d, wantSrc %d", srcCount, tt.wantSrc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
8113
server/swagger.json
8113
server/swagger.json
File diff suppressed because it is too large
Load Diff
|
@ -2,6 +2,7 @@
|
|||
parser: 'babel-eslint',
|
||||
plugins: [
|
||||
'react',
|
||||
'prettier',
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
|
@ -220,7 +221,7 @@
|
|||
'react/jsx-uses-react': 2,
|
||||
'react/jsx-uses-vars': 2,
|
||||
'react/no-danger': 2,
|
||||
'react/no-did-mount-set-state': 'error',
|
||||
'react/no-did-mount-set-state': 0,
|
||||
'react/no-did-update-set-state': 2,
|
||||
'react/no-direct-mutation-state': 2,
|
||||
'react/no-is-mounted': 2,
|
||||
|
@ -234,5 +235,11 @@
|
|||
'react/require-extension': 0,
|
||||
'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons)
|
||||
'react/sort-comp': 0, // TODO: 2
|
||||
'prettier/prettier': ['error', {
|
||||
'singleQuote': true,
|
||||
'trailingComma': 'es5',
|
||||
'bracketSpacing': false,
|
||||
'semi': false,
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ yarn add --dev packageName
|
|||
```
|
||||
|
||||
### Updating a package
|
||||
First, run
|
||||
First, run
|
||||
|
||||
```sh
|
||||
yarn outdated
|
||||
|
@ -31,3 +31,6 @@ To upgrade a single package named `packageName`:
|
|||
```sh
|
||||
yarn upgrade packageName
|
||||
```
|
||||
|
||||
## Testing
|
||||
Tests can be run via command line with `npm test`, from within the `/ui` directory. For more detailed reporting, use `npm test -- --reporters=verbose`.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var webpack = require('webpack')
|
||||
var path = require('path')
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
|
@ -15,6 +15,8 @@ module.exports = function(config) {
|
|||
'spec/spec-helper.js': ['webpack', 'sourcemap'],
|
||||
'spec/index.js': ['webpack', 'sourcemap'],
|
||||
},
|
||||
// For more detailed reporting on tests, you can add 'verbose' and/or 'progress'.
|
||||
// This can also be done via the command line with `npm test -- --reporters=verbose`.
|
||||
reporters: ['dots'],
|
||||
webpack: {
|
||||
devtool: 'inline-source-map',
|
||||
|
@ -35,7 +37,8 @@ module.exports = function(config) {
|
|||
exclude: /node_modules/,
|
||||
loader: 'style-loader!css-loader!sass-loader',
|
||||
},
|
||||
{ // Sinon behaves weirdly with webpack, see https://github.com/webpack/webpack/issues/304
|
||||
{
|
||||
// Sinon behaves weirdly with webpack, see https://github.com/webpack/webpack/issues/304
|
||||
test: /sinon\/pkg\/sinon\.js/,
|
||||
loader: 'imports?define=>false,require=>false',
|
||||
},
|
||||
|
@ -48,7 +51,7 @@ module.exports = function(config) {
|
|||
externals: {
|
||||
'react/addons': true,
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
'react/lib/ReactContext': true
|
||||
'react/lib/ReactContext': true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
@ -65,5 +68,5 @@ module.exports = function(config) {
|
|||
webpackServer: {
|
||||
noInfo: true, // please don't spam the console when running in karma!
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.3.4-0",
|
||||
"version": "1.3.5-0",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
@ -17,7 +17,8 @@
|
|||
"test:lint": "npm run lint; npm run test",
|
||||
"test:dev": "nodemon --exec npm run test:lint",
|
||||
"clean": "rm -rf build",
|
||||
"storybook": "node ./storybook"
|
||||
"storybook": "node ./storybook",
|
||||
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
|
||||
},
|
||||
"author": "",
|
||||
"eslintConfig": {
|
||||
|
@ -50,6 +51,7 @@
|
|||
"enzyme": "^2.4.1",
|
||||
"eslint": "3.9.1",
|
||||
"eslint-loader": "1.6.1",
|
||||
"eslint-plugin-prettier": "^2.1.2",
|
||||
"eslint-plugin-react": "6.6.0",
|
||||
"express": "^4.14.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
|
@ -66,6 +68,7 @@
|
|||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"karma-sinon-chai": "^1.2.4",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-verbose-reporter": "^0.0.6",
|
||||
"karma-webpack": "^1.8.0",
|
||||
"mocha": "^2.4.5",
|
||||
"mocha-loader": "^0.7.1",
|
||||
|
@ -76,6 +79,7 @@
|
|||
"postcss-loader": "^0.8.0",
|
||||
"postcss-reporter": "^1.3.1",
|
||||
"precss": "^1.4.0",
|
||||
"prettier": "^1.5.3",
|
||||
"react-addons-test-utils": "^15.0.2",
|
||||
"resolve-url-loader": "^1.6.0",
|
||||
"sass-loader": "^3.2.0",
|
||||
|
@ -102,7 +106,6 @@
|
|||
"node-uuid": "^1.4.7",
|
||||
"react": "^15.0.2",
|
||||
"react-addons-shallow-compare": "^15.0.2",
|
||||
"react-addons-update": "^15.1.0",
|
||||
"react-custom-scrollbars": "^4.1.1",
|
||||
"react-dimensions": "^1.2.0",
|
||||
"react-dom": "^15.0.2",
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
renameDashboardCell,
|
||||
syncDashboardCell,
|
||||
templateVariableSelected,
|
||||
cancelEditCell,
|
||||
} from 'src/dashboards/actions'
|
||||
|
||||
let state
|
||||
|
@ -62,6 +63,13 @@ const c1 = {
|
|||
isEditing: false,
|
||||
name: 'Gigawatts',
|
||||
}
|
||||
|
||||
const editingCell = {
|
||||
i: 1,
|
||||
isEditing: true,
|
||||
name: 'Edit me',
|
||||
}
|
||||
|
||||
const cells = [c1]
|
||||
const tempVar = {
|
||||
...d1.templates[0],
|
||||
|
@ -180,4 +188,17 @@ describe('DataExplorer.Reducers.UI', () => {
|
|||
expect(actual.dashboards[0].templates[0].values[1].selected).to.equal(false)
|
||||
expect(actual.dashboards[0].templates[0].values[2].selected).to.equal(true)
|
||||
})
|
||||
|
||||
it('can cancel cell editing', () => {
|
||||
const dash = _.cloneDeep(d1)
|
||||
dash.cells = [editingCell]
|
||||
|
||||
const actual = reducer(
|
||||
{dashboards: [dash]},
|
||||
cancelEditCell(dash.id, editingCell.i)
|
||||
)
|
||||
|
||||
expect(actual.dashboards[0].cells[0].isEditing).to.equal(false)
|
||||
expect(actual.dashboards[0].cells[0].name).to.equal(editingCell.name)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
|
||||
const fakeAddQueryAction = (panelID, queryID) => {
|
||||
return {
|
||||
type: 'ADD_QUERY',
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {panelID, queryID},
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ function buildInitialState(queryId, params) {
|
|||
return Object.assign({}, defaultQueryConfig(queryId), params)
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.queryConfig', () => {
|
||||
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
|
||||
const queryId = 123
|
||||
|
||||
it('can add a query', () => {
|
||||
|
@ -117,26 +117,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('when the query is part of a kapacitor rule', () => {
|
||||
it('only allows one field', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const isKapacitorRule = true
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(
|
||||
queryId,
|
||||
{field: 'a different field', funcs: []},
|
||||
isKapacitorRule
|
||||
)
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].field).to.equal('a different field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TOGGLE_FIELDS', () => {
|
||||
describe('DE_TOGGLE_FIELD', () => {
|
||||
it('can toggle multiple fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
|
@ -168,7 +149,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('APPLY_FUNCS_TO_FIELD', () => {
|
||||
describe('DE_APPLY_FUNCS_TO_FIELD', () => {
|
||||
it('applies functions to a field without any existing functions', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
|
@ -226,7 +207,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('CHOOSE_TAG', () => {
|
||||
describe('DE_CHOOSE_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
|
@ -287,7 +268,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('GROUP_BY_TAG', () => {
|
||||
describe('DE_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
|
@ -331,7 +312,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('TOGGLE_TAG_ACCEPTANCE', () => {
|
||||
describe('DE_TOGGLE_TAG_ACCEPTANCE', () => {
|
||||
it('it toggles areTagsAccepted', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
|
@ -346,7 +327,7 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('GROUP_BY_TIME', () => {
|
||||
describe('DE_GROUP_BY_TIME', () => {
|
||||
it('applys the appropriate group by time', () => {
|
||||
const time = '100y'
|
||||
const initialState = {
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
import reducer from 'src/kapacitor/reducers/queryConfigs'
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
chooseNamespace,
|
||||
chooseMeasurement,
|
||||
chooseTag,
|
||||
groupByTag,
|
||||
toggleTagAcceptance,
|
||||
toggleField,
|
||||
applyFuncsToField,
|
||||
groupByTime,
|
||||
} from 'src/kapacitor/actions/queryConfigs'
|
||||
|
||||
const fakeAddQueryAction = (panelID, queryID) => {
|
||||
return {
|
||||
type: 'KAPA_ADD_QUERY',
|
||||
payload: {panelID, queryID},
|
||||
}
|
||||
}
|
||||
|
||||
function buildInitialState(queryId, params) {
|
||||
return Object.assign({}, defaultQueryConfig(queryId), params)
|
||||
}
|
||||
|
||||
describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
|
||||
const queryId = 123
|
||||
|
||||
it('can add a query', () => {
|
||||
const state = reducer({}, fakeAddQueryAction('blah', queryId))
|
||||
|
||||
const actual = state[queryId]
|
||||
const expected = defaultQueryConfig(queryId)
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
describe('choosing db, rp, and measurement', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
})
|
||||
|
||||
it('sets the db and rp', () => {
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
database: 'telegraf',
|
||||
retentionPolicy: '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'))
|
||||
|
||||
expect(newState[queryId].measurement).to.equal('mem')
|
||||
})
|
||||
})
|
||||
|
||||
describe('a query has measurements and fields', () => {
|
||||
let state
|
||||
beforeEach(() => {
|
||||
const one = reducer({}, fakeAddQueryAction('any', queryId))
|
||||
const two = reducer(
|
||||
one,
|
||||
chooseNamespace(queryId, {
|
||||
database: '_internal',
|
||||
retentionPolicy: 'daily',
|
||||
})
|
||||
)
|
||||
const three = reducer(two, chooseMeasurement(queryId, 'disk'))
|
||||
state = reducer(
|
||||
three,
|
||||
toggleField(queryId, {field: '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)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseNamespace(queryId, {
|
||||
database: 'newdb',
|
||||
retentionPolicy: 'newrp',
|
||||
})
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
chooseMeasurement(queryId, 'newmeasurement')
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the query is part of a kapacitor rule', () => {
|
||||
it('only allows one field', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {field: 'a different field', funcs: []})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].field).to.equal('a different field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_TOGGLE_FIELD', () => {
|
||||
it('cannot toggle multiple fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {field: 'a different field', funcs: []})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields.length).to.equal(1)
|
||||
expect(newState[queryId].fields[0].field).to.equal('a different field')
|
||||
})
|
||||
|
||||
it('applies no funcs to newly selected fields', () => {
|
||||
expect(state[queryId].fields.length).to.equal(1)
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
toggleField(queryId, {field: 'a different field'})
|
||||
)
|
||||
|
||||
expect(newState[queryId].fields[0].funcs).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_APPLY_FUNCS_TO_FIELD', () => {
|
||||
it('applies functions to a field without any existing functions', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [
|
||||
{field: 'f1', funcs: ['fn1', 'fn2']},
|
||||
{field: 'f2', funcs: ['fn1']},
|
||||
],
|
||||
},
|
||||
}
|
||||
const action = applyFuncsToField(queryId, {
|
||||
field: 'f1',
|
||||
funcs: ['fn3', 'fn4'],
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fields).to.eql([
|
||||
{field: 'f1', funcs: ['fn3', 'fn4']},
|
||||
{field: 'f2', funcs: ['fn1']},
|
||||
])
|
||||
})
|
||||
|
||||
it('removes all functions and group by time when one field has no funcs applied', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [
|
||||
{field: 'f1', funcs: ['fn1', 'fn2']},
|
||||
{field: 'f2', funcs: ['fn3', 'fn4']},
|
||||
],
|
||||
groupBy: {
|
||||
time: '1m',
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const action = applyFuncsToField(queryId, {
|
||||
field: 'f1',
|
||||
funcs: [],
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].fields).to.eql([
|
||||
{field: 'f1', funcs: []},
|
||||
{field: 'f2', funcs: []},
|
||||
])
|
||||
expect(nextState[queryId].groupBy.time).to.equal(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_CHOOSE_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
tags: {
|
||||
k1: ['v0'],
|
||||
k2: ['foo'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
k1: ['v0', 'v1'],
|
||||
k2: ['foo'],
|
||||
})
|
||||
})
|
||||
|
||||
it("creates a new entry if it's the first key", () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
tags: {},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].tags).to.eql({
|
||||
k1: ['v1'],
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a value that is already in the list', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId, {
|
||||
tags: {
|
||||
k1: ['v1'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
const action = chooseTag(queryId, {
|
||||
key: 'k1',
|
||||
value: 'v1',
|
||||
})
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
// TODO: this should probably remove the `k1` property entirely from the tags object
|
||||
expect(nextState[queryId].tags).to.eql({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_GROUP_BY_TAG', () => {
|
||||
it('adds a tag key/value to the query', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: [], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: ['k1'],
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a tag if the given tag key is already in the GROUP BY list', () => {
|
||||
const initialState = {
|
||||
[queryId]: {
|
||||
id: 123,
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
fields: [],
|
||||
tags: {},
|
||||
groupBy: {tags: ['k1'], time: null},
|
||||
},
|
||||
}
|
||||
const action = groupByTag(queryId, 'k1')
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy).to.eql({
|
||||
time: null,
|
||||
tags: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_TOGGLE_TAG_ACCEPTANCE', () => {
|
||||
it('it toggles areTagsAccepted', () => {
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
}
|
||||
const action = toggleTagAcceptance(queryId)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].areTagsAccepted).to.equal(
|
||||
!initialState[queryId].areTagsAccepted
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('KAPA_GROUP_BY_TIME', () => {
|
||||
it('applys the appropriate group by time', () => {
|
||||
const time = '100y'
|
||||
const initialState = {
|
||||
[queryId]: buildInitialState(queryId),
|
||||
}
|
||||
|
||||
const action = groupByTime(queryId, time)
|
||||
|
||||
const nextState = reducer(initialState, action)
|
||||
|
||||
expect(nextState[queryId].groupBy.time).to.equal(time)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -11,6 +11,7 @@ import {
|
|||
updateMessage,
|
||||
updateAlerts,
|
||||
updateAlertNodes,
|
||||
updateAlertProperty,
|
||||
updateRuleName,
|
||||
deleteRuleSuccess,
|
||||
updateRuleStatusSuccess,
|
||||
|
@ -200,6 +201,106 @@ describe('Kapacitor.Reducers.rules', () => {
|
|||
expect(newState[ruleID].details).to.equal(details)
|
||||
})
|
||||
|
||||
it('can update properties', () => {
|
||||
const ruleID = 1
|
||||
|
||||
const alertNodeName = 'pushover'
|
||||
|
||||
const alertProperty1_Name = 'device'
|
||||
const alertProperty1_ArgsOrig =
|
||||
'pineapple_kingdom_control_room,bob_cOreos_watch'
|
||||
const alertProperty1_ArgsDiff = 'pineapple_kingdom_control_tower'
|
||||
|
||||
const alertProperty2_Name = 'URLTitle'
|
||||
const alertProperty2_ArgsOrig = 'Cubeapple Rising'
|
||||
const alertProperty2_ArgsDiff = 'Cubeapple Falling'
|
||||
|
||||
const alertProperty1_Orig = {
|
||||
name: alertProperty1_Name,
|
||||
args: [alertProperty1_ArgsOrig],
|
||||
}
|
||||
const alertProperty1_Diff = {
|
||||
name: alertProperty1_Name,
|
||||
args: [alertProperty1_ArgsDiff],
|
||||
}
|
||||
const alertProperty2_Orig = {
|
||||
name: alertProperty2_Name,
|
||||
args: [alertProperty2_ArgsOrig],
|
||||
}
|
||||
const alertProperty2_Diff = {
|
||||
name: alertProperty2_Name,
|
||||
args: [alertProperty2_ArgsDiff],
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
[ruleID]: {
|
||||
id: ruleID,
|
||||
alertNodes: [
|
||||
{
|
||||
name: 'pushover',
|
||||
args: null,
|
||||
properties: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const getAlertPropertyArgs = (matchState, propertyName) =>
|
||||
matchState[ruleID].alertNodes
|
||||
.find(node => node.name === alertNodeName)
|
||||
.properties.find(property => property.name === propertyName).args[0]
|
||||
|
||||
// add first property
|
||||
let newState = reducer(
|
||||
initialState,
|
||||
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Orig)
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
|
||||
alertProperty1_ArgsOrig
|
||||
)
|
||||
|
||||
// change first property
|
||||
newState = reducer(
|
||||
initialState,
|
||||
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Diff)
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
|
||||
alertProperty1_ArgsDiff
|
||||
)
|
||||
|
||||
// add second property
|
||||
newState = reducer(
|
||||
initialState,
|
||||
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Orig)
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
|
||||
alertProperty1_ArgsDiff
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
|
||||
alertProperty2_ArgsOrig
|
||||
)
|
||||
expect(
|
||||
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
|
||||
.properties.length
|
||||
).to.equal(2)
|
||||
|
||||
// change second property
|
||||
newState = reducer(
|
||||
initialState,
|
||||
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Diff)
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
|
||||
alertProperty1_ArgsDiff
|
||||
)
|
||||
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
|
||||
alertProperty2_ArgsDiff
|
||||
)
|
||||
expect(
|
||||
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
|
||||
.properties.length
|
||||
).to.equal(2)
|
||||
})
|
||||
|
||||
it('can update status', () => {
|
||||
const ruleID = 1
|
||||
const status = 'enabled'
|
||||
|
|
|
@ -88,11 +88,17 @@ const AdminTabs = ({
|
|||
return (
|
||||
<Tabs className="row">
|
||||
<TabList customClass="col-md-2 admin-tabs">
|
||||
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
|
||||
{tabs.map((t, i) =>
|
||||
<Tab key={tabs[i].type}>
|
||||
{tabs[i].type}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanels customClass="col-md-10 admin-tabs--content">
|
||||
{tabs.map((t, i) =>
|
||||
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
|
||||
<TabPanel key={tabs[i].type}>
|
||||
{t.component}
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
|
|
@ -52,9 +52,7 @@ const DatabaseTable = ({
|
|||
<table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Retention Policy
|
||||
</th>
|
||||
<th>Retention Policy</th>
|
||||
<th style={{width: `${DATABASE_TABLE.colDuration}px`}}>
|
||||
Duration
|
||||
</th>
|
||||
|
|
|
@ -105,7 +105,9 @@ const Header = ({
|
|||
|
||||
return (
|
||||
<div className="db-manager-header">
|
||||
<h4>{database.name}</h4>
|
||||
<h4>
|
||||
{database.name}
|
||||
</h4>
|
||||
{database.hasOwnProperty('deleteCode') ? deleteConfirmation : buttons}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,9 @@ import React, {PropTypes} from 'react'
|
|||
const EmptyRow = ({tableName}) =>
|
||||
<tr className="table-empty-state">
|
||||
<th colSpan="5">
|
||||
<p>You don't have any {tableName},<br />why not create one?</p>
|
||||
<p>
|
||||
You don't have any {tableName},<br />why not create one?
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ const QueriesTable = ({queries, onKillQuery}) =>
|
|||
Database
|
||||
</th>
|
||||
<th>Query</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colRunning}px`}}>
|
||||
Running
|
||||
</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colRunning}px`}}>Running</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colKillQuery}px`}} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
@ -39,7 +39,11 @@ class QueryRow extends Component {
|
|||
>
|
||||
{database}
|
||||
</td>
|
||||
<td><code>{query}</code></td>
|
||||
<td>
|
||||
<code>
|
||||
{query}
|
||||
</code>
|
||||
</td>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colRunning}px`}}
|
||||
className="monotype"
|
||||
|
|
|
@ -61,7 +61,9 @@ const RoleRow = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: `${ROLES_TABLE.colName}px`}}>{name}</td>
|
||||
<td style={{width: `${ROLES_TABLE.colName}px`}}>
|
||||
{name}
|
||||
</td>
|
||||
<td>
|
||||
{allPermissions && allPermissions.length
|
||||
? <MultiSelectDropdown
|
||||
|
|
|
@ -71,7 +71,9 @@ const UserRow = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: `${USERS_TABLE.colUsername}px`}}>{name}</td>
|
||||
<td style={{width: `${USERS_TABLE.colUsername}px`}}>
|
||||
{name}
|
||||
</td>
|
||||
<td style={{width: `${USERS_TABLE.colPassword}px`}}>
|
||||
<ChangePassRow
|
||||
onEdit={onEdit}
|
||||
|
|
|
@ -166,9 +166,7 @@ class AdminPage extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Admin
|
||||
</h1>
|
||||
<h1 className="page-header__title">Admin</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
|
|
|
@ -48,9 +48,8 @@ class AlertsTable extends Component {
|
|||
changeSort(key) {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
if (this.state.sortKey === key) {
|
||||
const reverseDirection = this.state.sortDirection === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
const reverseDirection =
|
||||
this.state.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
this.setState({sortDirection: reverseDirection})
|
||||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
|
@ -192,9 +191,7 @@ class AlertsTable extends Component {
|
|||
</p>
|
||||
</div>
|
||||
: <div className="generic-empty-state">
|
||||
<h4 className="no-user-select">
|
||||
There are no Alerts to display
|
||||
</h4>
|
||||
<h4 className="no-user-select">There are no Alerts to display</h4>
|
||||
<br />
|
||||
<h6 className="no-user-select">
|
||||
Try changing the Time Range or
|
||||
|
@ -236,7 +233,9 @@ class AlertsTable extends Component {
|
|||
</div>
|
||||
: <div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
<h2 className="panel-title">
|
||||
{this.props.alerts.length} Alerts
|
||||
</h2>
|
||||
{this.props.alerts.length
|
||||
? <SearchBar onSearch={this.filterAlerts} />
|
||||
: null}
|
||||
|
|
|
@ -27,7 +27,6 @@ class AlertsApp extends Component {
|
|||
loading: true,
|
||||
hasKapacitor: false,
|
||||
alerts: [],
|
||||
isTimeOpen: false,
|
||||
timeRange: {
|
||||
upper: moment().format(),
|
||||
lower: moment().subtract(lowerInSec || oneDayInSec, 'seconds').format(),
|
||||
|
@ -40,8 +39,6 @@ class AlertsApp extends Component {
|
|||
this.fetchAlerts = ::this.fetchAlerts
|
||||
this.renderSubComponents = ::this.renderSubComponents
|
||||
this.handleGetMoreAlerts = ::this.handleGetMoreAlerts
|
||||
this.handleToggleTime = ::this.handleToggleTime
|
||||
this.handleCloseTime = ::this.handleCloseTime
|
||||
this.handleApplyTime = ::this.handleApplyTime
|
||||
}
|
||||
|
||||
|
@ -138,14 +135,6 @@ class AlertsApp extends Component {
|
|||
: <NoKapacitorError source={source} />
|
||||
}
|
||||
|
||||
handleToggleTime() {
|
||||
this.setState({isTimeOpen: !this.state.isTimeOpen})
|
||||
}
|
||||
|
||||
handleCloseTime() {
|
||||
this.setState({isTimeOpen: false})
|
||||
}
|
||||
|
||||
handleApplyTime(timeRange) {
|
||||
this.setState({timeRange})
|
||||
}
|
||||
|
@ -164,16 +153,11 @@ class AlertsApp extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Alert History
|
||||
</h1>
|
||||
<h1 className="page-header__title">Alert History</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
<CustomTimeRangeDropdown
|
||||
isVisible={this.state.isTimeOpen}
|
||||
onToggle={this.handleToggleTime}
|
||||
onClose={this.handleCloseTime}
|
||||
onApplyTimeRange={this.handleApplyTime}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
|
|
|
@ -15,7 +15,9 @@ const Login = ({authData: {auth}}) => {
|
|||
<div className="auth-box">
|
||||
<div className="auth-logo" />
|
||||
<h1 className="auth-text-logo">Chronograf</h1>
|
||||
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p>
|
||||
<p>
|
||||
<strong>{VERSION}</strong> / Time-Series Data Visualization
|
||||
</p>
|
||||
{auth.links &&
|
||||
auth.links.map(({name, login, label}) =>
|
||||
<a key={name} className="btn btn-primary" href={login}>
|
||||
|
|
|
@ -14,9 +14,7 @@ import {errorThrown} from 'shared/actions/errors'
|
|||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||
|
||||
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||
import {
|
||||
makeQueryForTemplate,
|
||||
} from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||
import {makeQueryForTemplate} from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||
import parsers from 'shared/parsing'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
|
@ -93,6 +91,14 @@ export const editDashboardCell = (dashboard, x, y, isEditing) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const cancelEditCell = (dashboardID, cellID) => ({
|
||||
type: 'CANCEL_EDIT_CELL',
|
||||
payload: {
|
||||
dashboardID,
|
||||
cellID,
|
||||
},
|
||||
})
|
||||
|
||||
export const renameDashboardCell = (dashboard, x, y, name) => ({
|
||||
type: 'RENAME_DASHBOARD_CELL',
|
||||
payload: {
|
||||
|
|
|
@ -23,22 +23,20 @@ const Dashboard = ({
|
|||
onSummonOverlayTechnologies,
|
||||
onSelectTemplate,
|
||||
showTemplateControlBar,
|
||||
onCancelEditCell,
|
||||
}) => {
|
||||
const cells = dashboard.cells.map(cell => {
|
||||
const dashboardCell = {...cell}
|
||||
dashboardCell.queries = dashboardCell.queries.map(({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
}) => ({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
database: db,
|
||||
text: query,
|
||||
}))
|
||||
dashboardCell.queries = dashboardCell.queries.map(
|
||||
({label, query, queryConfig, db}) => ({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
database: db,
|
||||
text: query,
|
||||
})
|
||||
)
|
||||
return dashboardCell
|
||||
})
|
||||
|
||||
|
@ -59,6 +57,7 @@ const Dashboard = ({
|
|||
/>}
|
||||
{cells.length
|
||||
? <LayoutRenderer
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
templates={templatesIncludingDashTime}
|
||||
isEditable={true}
|
||||
cells={cells}
|
||||
|
@ -127,6 +126,7 @@ Dashboard.propTypes = {
|
|||
onOpenTemplateManager: func.isRequired,
|
||||
onSelectTemplate: func.isRequired,
|
||||
showTemplateControlBar: bool,
|
||||
onCancelEditCell: func,
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
|
|
|
@ -35,7 +35,9 @@ const DashboardHeader = ({
|
|||
type="button"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<span>{buttonText}</span>
|
||||
<span>
|
||||
{buttonText}
|
||||
</span>
|
||||
<span className="caret" />
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
|
@ -99,7 +101,10 @@ DashboardHeader.propTypes = {
|
|||
buttonText: string,
|
||||
dashboard: shape({}),
|
||||
headerText: string,
|
||||
timeRange: shape({}).isRequired,
|
||||
timeRange: shape({
|
||||
lower: string,
|
||||
upper: string,
|
||||
}).isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
isHidden: bool.isRequired,
|
||||
handleChooseTimeRange: func.isRequired,
|
||||
|
|
|
@ -7,9 +7,7 @@ const DashboardsHeader = ({sourceName}) => {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Dashboards
|
||||
</h1>
|
||||
<h1 className="page-header__title">Dashboards</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={sourceName} />
|
||||
|
|
|
@ -25,7 +25,9 @@ const DashboardsPageContents = ({
|
|||
<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>
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
|
|
|
@ -34,9 +34,7 @@ const DashboardsTable = ({
|
|||
{tv.tempVar}
|
||||
</code>
|
||||
)
|
||||
: <span className="empty-string">
|
||||
None
|
||||
</span>}
|
||||
: <span className="empty-string">None</span>}
|
||||
</td>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
|
|
|
@ -90,7 +90,11 @@ const TemplateQueryBuilder = ({
|
|||
</div>
|
||||
)
|
||||
default:
|
||||
return <div><span className="tvm-query-builder--text">n/a</span></div>
|
||||
return (
|
||||
<div>
|
||||
<span className="tvm-query-builder--text">n/a</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -198,7 +198,9 @@ const TableInput = ({
|
|||
/>
|
||||
</div>
|
||||
: <div style={{width: '100%'}} onClick={() => onStartEdit(name)}>
|
||||
<div className="tvm-input">{defaultValue}</div>
|
||||
<div className="tvm-input">
|
||||
{defaultValue}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,7 @@ import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
|
|||
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
|
||||
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
|
||||
import Dashboard from 'src/dashboards/components/Dashboard'
|
||||
import TemplateVariableManager
|
||||
from 'src/dashboards/components/TemplateVariableManager'
|
||||
import TemplateVariableManager from 'src/dashboards/components/TemplateVariableManager'
|
||||
|
||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
|
||||
|
@ -45,7 +44,6 @@ class DashboardPage extends Component {
|
|||
this.handleCancelEditDashboard = ::this.handleCancelEditDashboard
|
||||
this.handleDeleteDashboardCell = ::this.handleDeleteDashboardCell
|
||||
this.handleOpenTemplateManager = ::this.handleOpenTemplateManager
|
||||
this.handleRenameDashboardCell = ::this.handleRenameDashboardCell
|
||||
this.handleUpdateDashboardCell = ::this.handleUpdateDashboardCell
|
||||
this.handleCloseTemplateManager = ::this.handleCloseTemplateManager
|
||||
this.handleSummonOverlayTechnologies = ::this
|
||||
|
@ -104,8 +102,8 @@ class DashboardPage extends Component {
|
|||
this.setState({selectedCell: cell})
|
||||
}
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
this.props.dashboardActions.setTimeRange({lower, upper: null})
|
||||
handleChooseTimeRange(timeRange) {
|
||||
this.props.dashboardActions.setTimeRange(timeRange)
|
||||
}
|
||||
|
||||
handleUpdatePosition(cells) {
|
||||
|
@ -145,26 +143,12 @@ class DashboardPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleRenameDashboardCell(x, y) {
|
||||
return evt => {
|
||||
this.props.dashboardActions.renameDashboardCell(
|
||||
this.getActiveDashboard(),
|
||||
x,
|
||||
y,
|
||||
evt.target.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateDashboardCell(newCell) {
|
||||
return () => {
|
||||
this.props.dashboardActions.editDashboardCell(
|
||||
this.props.dashboardActions.updateDashboardCell(
|
||||
this.getActiveDashboard(),
|
||||
newCell.x,
|
||||
newCell.y,
|
||||
false
|
||||
newCell
|
||||
)
|
||||
this.props.dashboardActions.putDashboard(this.getActiveDashboard())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,6 +223,13 @@ class DashboardPage extends Component {
|
|||
this.props.templateControlBarVisibilityToggled()
|
||||
}
|
||||
|
||||
handleCancelEditCell(cellID) {
|
||||
this.props.dashboardActions.cancelEditCell(
|
||||
this.getActiveDashboard().id,
|
||||
cellID
|
||||
)
|
||||
}
|
||||
|
||||
getActiveDashboard() {
|
||||
const {params: {dashboardID}, dashboards} = this.props
|
||||
return dashboards.find(d => d.id === +dashboardID)
|
||||
|
@ -248,6 +239,7 @@ class DashboardPage extends Component {
|
|||
const {
|
||||
source,
|
||||
timeRange,
|
||||
timeRange: {lower, upper},
|
||||
showTemplateControlBar,
|
||||
dashboards,
|
||||
autoRefresh,
|
||||
|
@ -259,15 +251,30 @@ class DashboardPage extends Component {
|
|||
params: {sourceID},
|
||||
} = this.props
|
||||
|
||||
const dashboard = this.getActiveDashboard()
|
||||
const lowerType = lower && lower.includes('Z') ? 'timeStamp' : 'constant'
|
||||
const upperType = upper && upper.includes('Z') ? 'timeStamp' : 'constant'
|
||||
|
||||
const dashboardTime = {
|
||||
id: 'dashtime',
|
||||
tempVar: ':dashboardTime:',
|
||||
type: 'constant',
|
||||
type: lowerType,
|
||||
values: [
|
||||
{
|
||||
value: timeRange.lower,
|
||||
type: 'constant',
|
||||
value: lower,
|
||||
type: lowerType,
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const upperDashboardTime = {
|
||||
id: 'upperdashtime',
|
||||
tempVar: ':upperDashboardTime:',
|
||||
type: upperType,
|
||||
values: [
|
||||
{
|
||||
value: upper || 'now()',
|
||||
type: upperType,
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
|
@ -283,8 +290,19 @@ class DashboardPage extends Component {
|
|||
values: [],
|
||||
}
|
||||
|
||||
const templatesIncludingDashTime = (dashboard &&
|
||||
dashboard.templates.concat(dashboardTime).concat(interval)) || []
|
||||
const dashboard = this.getActiveDashboard()
|
||||
|
||||
let templatesIncludingDashTime
|
||||
if (dashboard) {
|
||||
templatesIncludingDashTime = [
|
||||
...dashboard.templates,
|
||||
dashboardTime,
|
||||
upperDashboardTime,
|
||||
interval,
|
||||
]
|
||||
} else {
|
||||
templatesIncludingDashTime = []
|
||||
}
|
||||
|
||||
const {selectedCell, isEditMode, isTemplating} = this.state
|
||||
|
||||
|
@ -337,13 +355,13 @@ class DashboardPage extends Component {
|
|||
showTemplateControlBar={showTemplateControlBar}
|
||||
>
|
||||
{dashboards
|
||||
? dashboards.map((d, i) => (
|
||||
? dashboards.map((d, i) =>
|
||||
<li className="dropdown-item" key={i}>
|
||||
<Link to={`/sources/${sourceID}/dashboards/${d.id}`}>
|
||||
{d.name}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)
|
||||
: null}
|
||||
</DashboardHeader>}
|
||||
{dashboard
|
||||
|
@ -358,13 +376,13 @@ class DashboardPage extends Component {
|
|||
onEditCell={this.handleEditDashboardCell}
|
||||
onPositionChange={this.handleUpdatePosition}
|
||||
onDeleteCell={this.handleDeleteDashboardCell}
|
||||
onRenameCell={this.handleRenameDashboardCell}
|
||||
onUpdateCell={this.handleUpdateDashboardCell}
|
||||
onOpenTemplateManager={this.handleOpenTemplateManager}
|
||||
templatesIncludingDashTime={templatesIncludingDashTime}
|
||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||
onSelectTemplate={this.handleSelectTemplate}
|
||||
showTemplateControlBar={showTemplateControlBar}
|
||||
onCancelEditCell={::this.handleCancelEditCell}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
@ -394,7 +412,7 @@ DashboardPage.propTypes = {
|
|||
setTimeRange: func.isRequired,
|
||||
addDashboardCellAsync: func.isRequired,
|
||||
editDashboardCell: func.isRequired,
|
||||
renameDashboardCell: func.isRequired,
|
||||
cancelEditCell: func.isRequired,
|
||||
}).isRequired,
|
||||
dashboards: arrayOf(
|
||||
shape({
|
||||
|
|
|
@ -132,6 +132,24 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'CANCEL_EDIT_CELL': {
|
||||
const {dashboardID, cellID} = action.payload
|
||||
|
||||
const dashboards = state.dashboards.map(
|
||||
d =>
|
||||
d.id === dashboardID
|
||||
? {
|
||||
...d,
|
||||
cells: d.cells.map(
|
||||
c => (c.i === cellID ? {...c, isEditing: false} : c)
|
||||
),
|
||||
}
|
||||
: d
|
||||
)
|
||||
|
||||
return {...state, dashboards}
|
||||
}
|
||||
|
||||
case 'SYNC_DASHBOARD_CELL': {
|
||||
const {cell, dashboard} = action.payload
|
||||
|
||||
|
@ -217,12 +235,12 @@ export default function ui(state = initialState, action) {
|
|||
|
||||
const dashboards = state.dashboards.map(
|
||||
dashboard =>
|
||||
(dashboard.id === dashboardID
|
||||
dashboard.id === dashboardID
|
||||
? {
|
||||
...dashboard,
|
||||
templates: dashboard.templates.map(
|
||||
template =>
|
||||
(template.id === templateID
|
||||
template.id === templateID
|
||||
? {
|
||||
...template,
|
||||
values: values.map((value, i) => ({
|
||||
|
@ -231,10 +249,10 @@ export default function ui(state = initialState, action) {
|
|||
type: TEMPLATE_VARIABLE_TYPES[template.type],
|
||||
})),
|
||||
}
|
||||
: template)
|
||||
: template
|
||||
),
|
||||
}
|
||||
: dashboard)
|
||||
: dashboard
|
||||
)
|
||||
|
||||
return {...state, dashboards}
|
||||
|
|
|
@ -3,156 +3,130 @@ import uuid from 'node-uuid'
|
|||
import {getQueryConfig} from 'shared/apis'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {DEFAULT_DATA_EXPLORER_GROUP_BY_INTERVAL} from 'src/data_explorer/constants'
|
||||
|
||||
export function addQuery(options = {}) {
|
||||
return {
|
||||
type: 'ADD_QUERY',
|
||||
payload: {
|
||||
queryID: uuid.v4(),
|
||||
options,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const addQuery = (options = {}) => ({
|
||||
type: 'DE_ADD_QUERY',
|
||||
payload: {
|
||||
queryID: uuid.v4(),
|
||||
options,
|
||||
},
|
||||
})
|
||||
|
||||
export function deleteQuery(queryID) {
|
||||
return {
|
||||
type: 'DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const deleteQuery = queryID => ({
|
||||
type: 'DE_DELETE_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export function toggleField(queryId, fieldFunc, isKapacitorRule) {
|
||||
return {
|
||||
type: 'TOGGLE_FIELD',
|
||||
meta: {
|
||||
isKapacitorRule,
|
||||
},
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const toggleField = (queryId, fieldFunc) => ({
|
||||
type: 'DE_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryId, time) => ({
|
||||
type: 'DE_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryId,
|
||||
time,
|
||||
},
|
||||
})
|
||||
|
||||
// all fields implicitly have a function applied to them, so consequently
|
||||
// we need to set the auto group by time
|
||||
export const toggleFieldWithGroupByInterval = (queryID, fieldFunc, isKapacitorRule) => (dispatch) => {
|
||||
dispatch(toggleField(queryID, fieldFunc, isKapacitorRule))
|
||||
export const toggleFieldWithGroupByInterval = (
|
||||
queryID,
|
||||
fieldFunc
|
||||
) => dispatch => {
|
||||
dispatch(toggleField(queryID, fieldFunc))
|
||||
dispatch(groupByTime(queryID, DEFAULT_DATA_EXPLORER_GROUP_BY_INTERVAL))
|
||||
}
|
||||
|
||||
export function groupByTime(queryId, time) {
|
||||
return {
|
||||
type: 'GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryId,
|
||||
time,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const applyFuncsToField = (queryId, fieldFunc) => ({
|
||||
type: 'DE_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export function applyFuncsToField(queryId, fieldFunc, isInDataExplorer) {
|
||||
return {
|
||||
type: 'APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
isInDataExplorer,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const chooseTag = (queryId, tag) => ({
|
||||
type: 'DE_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export function chooseTag(queryId, tag) {
|
||||
return {
|
||||
type: 'CHOOSE_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tag,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({
|
||||
type: 'DE_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryId,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export function chooseNamespace(queryId, {database, retentionPolicy}) {
|
||||
return {
|
||||
type: 'CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryId,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const chooseMeasurement = (queryId, measurement) => ({
|
||||
type: 'DE_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryId,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export function chooseMeasurement(queryId, measurement) {
|
||||
return {
|
||||
type: 'CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryId,
|
||||
measurement,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const editRawText = (queryId, rawText) => ({
|
||||
type: 'DE_EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryId,
|
||||
rawText,
|
||||
},
|
||||
})
|
||||
|
||||
export function editRawText(queryId, rawText) {
|
||||
return {
|
||||
type: 'EDIT_RAW_TEXT',
|
||||
payload: {
|
||||
queryId,
|
||||
rawText,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const setTimeRange = bounds => ({
|
||||
type: 'DE_SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
})
|
||||
|
||||
export function setTimeRange(bounds) {
|
||||
return {
|
||||
type: 'SET_TIME_RANGE',
|
||||
payload: {
|
||||
bounds,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const groupByTag = (queryId, tagKey) => ({
|
||||
type: 'DE_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export function groupByTag(queryId, tagKey) {
|
||||
return {
|
||||
type: 'GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tagKey,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const toggleTagAcceptance = queryId => ({
|
||||
type: 'DE_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryId,
|
||||
},
|
||||
})
|
||||
|
||||
export function toggleTagAcceptance(queryId) {
|
||||
return {
|
||||
type: 'TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRawQuery(queryID, text) {
|
||||
return {
|
||||
type: 'UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const updateRawQuery = (queryID, text) => ({
|
||||
type: 'DE_UPDATE_RAW_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateQueryConfig = config => ({
|
||||
type: 'UPDATE_QUERY_CONFIG',
|
||||
type: 'DE_UPDATE_QUERY_CONFIG',
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
})
|
||||
|
||||
export const editQueryStatus = (queryID, status) => ({
|
||||
type: 'EDIT_QUERY_STATUS',
|
||||
type: 'DE_EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
|
|
|
@ -7,12 +7,7 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
import {showFieldKeys} from 'shared/apis/metaQuery'
|
||||
import showFieldKeysParser from 'shared/parsing/showFieldKeys'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
const FieldList = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -112,7 +107,9 @@ const FieldList = React.createClass({
|
|||
if (!database || !measurement) {
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Measurement</strong> selected</span>
|
||||
<span>
|
||||
No <strong>Measurement</strong> selected
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,18 @@ const GroupByTimeDropdown = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {selected, onChooseGroupByTime, isInRuleBuilder, isInDataExplorer} = this.props
|
||||
const {
|
||||
selected,
|
||||
onChooseGroupByTime,
|
||||
isInRuleBuilder,
|
||||
isInDataExplorer,
|
||||
} = this.props
|
||||
|
||||
let validOptions = groupByTimeOptions
|
||||
if (isInDataExplorer) {
|
||||
validOptions = validOptions.filter(({menuOption}) => menuOption !== DEFAULT_DASHBOARD_GROUP_BY_INTERVAL)
|
||||
if (isInDataExplorer || isInRuleBuilder) {
|
||||
validOptions = validOptions.filter(
|
||||
({menuOption}) => menuOption !== DEFAULT_DASHBOARD_GROUP_BY_INTERVAL
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -112,7 +112,9 @@ const MeasurementList = React.createClass({
|
|||
if (!this.props.query.database) {
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Database</strong> selected</span>
|
||||
<span>
|
||||
No <strong>Database</strong> selected
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,12 @@ const NoDataNodeError = React.createClass({
|
|||
render() {
|
||||
return (
|
||||
<ClusterError>
|
||||
<PanelHeading>{errorCopy.noData.head}</PanelHeading>
|
||||
<PanelBody>{errorCopy.noData.body}</PanelBody>
|
||||
<PanelHeading>
|
||||
{errorCopy.noData.head}
|
||||
</PanelHeading>
|
||||
<PanelBody>
|
||||
{errorCopy.noData.body}
|
||||
</PanelBody>
|
||||
</ClusterError>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -52,7 +52,10 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
this.props.actions.toggleFieldWithGroupByInterval(this.props.query.id, field)
|
||||
this.props.actions.toggleFieldWithGroupByInterval(
|
||||
this.props.query.id,
|
||||
field
|
||||
)
|
||||
},
|
||||
|
||||
handleGroupByTime(time) {
|
||||
|
@ -60,7 +63,11 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
handleApplyFuncsToField(fieldFunc) {
|
||||
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc, this.props.isInDataExplorer)
|
||||
this.props.actions.applyFuncsToField(
|
||||
this.props.query.id,
|
||||
fieldFunc,
|
||||
this.props.isInDataExplorer
|
||||
)
|
||||
},
|
||||
|
||||
handleChooseTag(tag) {
|
||||
|
|
|
@ -225,7 +225,9 @@ class QueryEditor extends Component {
|
|||
className={classnames('varmoji', {'varmoji-rotated': isTemplating})}
|
||||
>
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">{this.renderStatus(status)}</div>
|
||||
<div className="varmoji-front">
|
||||
{this.renderStatus(status)}
|
||||
</div>
|
||||
<div className="varmoji-back">
|
||||
{isTemplating
|
||||
? <TemplateDrawer
|
||||
|
|
|
@ -30,7 +30,9 @@ const QueryMakerTab = React.createClass({
|
|||
})}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
<label>{this.props.queryTabText}</label>
|
||||
<label>
|
||||
{this.props.queryTabText}
|
||||
</label>
|
||||
<span className="query-maker--delete" onClick={this.handleDelete} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -27,10 +27,18 @@ const CustomCell = React.createClass({
|
|||
if (columnName === 'time') {
|
||||
const date = moment(new Date(data)).format('MM/DD/YY hh:mm:ssA')
|
||||
|
||||
return <span>{date}</span>
|
||||
return (
|
||||
<span>
|
||||
{date}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <span>{data}</span>
|
||||
return (
|
||||
<span>
|
||||
{data}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -137,9 +145,8 @@ const ChronoTable = React.createClass({
|
|||
const headerHeight = 30
|
||||
const minWidth = 70
|
||||
const styleAdjustedHeight = height - stylePixelOffset
|
||||
const width = columns && columns.length > 1
|
||||
? defaultColumnWidth
|
||||
: containerWidth
|
||||
const width =
|
||||
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
|
||||
|
||||
if (!query) {
|
||||
return <div className="generic-empty-state">Please add a query below</div>
|
||||
|
@ -172,9 +179,7 @@ const ChronoTable = React.createClass({
|
|||
/>}
|
||||
<div className="table--tabs-content">
|
||||
{(columns && !columns.length) || (values && !values.length)
|
||||
? <div className="generic-empty-state">
|
||||
This series is empty
|
||||
</div>
|
||||
? <div className="generic-empty-state">This series is empty</div>
|
||||
: <Table
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
isColumnResizing={false}
|
||||
|
@ -191,7 +196,11 @@ const ChronoTable = React.createClass({
|
|||
isResizable={true}
|
||||
key={columnName}
|
||||
columnKey={columnName}
|
||||
header={<Cell>{columnName}</Cell>}
|
||||
header={
|
||||
<Cell>
|
||||
{columnName}
|
||||
</Cell>
|
||||
}
|
||||
cell={({rowIndex}) =>
|
||||
<CustomCell
|
||||
columnName={columnName}
|
||||
|
|
|
@ -17,7 +17,9 @@ const VisHeader = ({views, view, onToggleView, name}) =>
|
|||
)}
|
||||
</ul>
|
||||
: null}
|
||||
<div className="graph-title">{name}</div>
|
||||
<div className="graph-title">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {arrayOf, func, string} = PropTypes
|
||||
|
|
|
@ -12,7 +12,8 @@ const WriteDataBody = ({
|
|||
isManual,
|
||||
fileInput,
|
||||
handleFileOpen,
|
||||
}) => (
|
||||
isUploading,
|
||||
}) =>
|
||||
<div className="write-data-form--body">
|
||||
{isManual
|
||||
? <textarea
|
||||
|
@ -45,13 +46,13 @@ const WriteDataBody = ({
|
|||
</span>}
|
||||
</div>}
|
||||
<WriteDataFooter
|
||||
isUploading={isUploading}
|
||||
isManual={isManual}
|
||||
inputContent={inputContent}
|
||||
handleSubmit={handleSubmit}
|
||||
uploadContent={uploadContent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
|
@ -66,6 +67,7 @@ WriteDataBody.propTypes = {
|
|||
isManual: bool,
|
||||
fileInput: func.isRequired,
|
||||
handleFileOpen: func.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
}
|
||||
|
||||
export default WriteDataBody
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const submitButton = 'btn btn-sm btn-success write-data-form--submit'
|
||||
const spinner = 'btn-spinner'
|
||||
|
||||
const WriteDataFooter = ({
|
||||
isManual,
|
||||
inputContent,
|
||||
uploadContent,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
isUploading,
|
||||
}) =>
|
||||
<div className="write-data-form--footer">
|
||||
{isManual
|
||||
? <span className="write-data-form--helper">
|
||||
|
@ -26,19 +30,23 @@ const WriteDataFooter = ({
|
|||
</a>
|
||||
</span>}
|
||||
<button
|
||||
className="btn btn-sm btn-success write-data-form--submit"
|
||||
className={isUploading ? `${submitButton} ${spinner}` : submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={(!inputContent && isManual) || (!uploadContent && !isManual)}
|
||||
disabled={
|
||||
(!inputContent && isManual) ||
|
||||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
}
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
WriteDataFooter.propTypes = {
|
||||
isManual: bool.isRequired,
|
||||
isUploading: bool.isRequired,
|
||||
uploadContent: string,
|
||||
inputContent: string,
|
||||
handleSubmit: func,
|
||||
|
|
|
@ -19,6 +19,7 @@ class WriteDataForm extends Component {
|
|||
progress: '',
|
||||
isManual: false,
|
||||
dragClass: 'drag-none',
|
||||
isUploading: false,
|
||||
}
|
||||
|
||||
this.handleSelectDatabase = ::this.handleSelectDatabase
|
||||
|
@ -59,12 +60,15 @@ class WriteDataForm extends Component {
|
|||
const {onClose, source, writeLineProtocol} = this.props
|
||||
const {inputContent, uploadContent, selectedDatabase, isManual} = this.state
|
||||
const content = isManual ? inputContent : uploadContent
|
||||
this.setState({isUploading: true})
|
||||
|
||||
try {
|
||||
await writeLineProtocol(source, selectedDatabase, content)
|
||||
this.setState({isUploading: false})
|
||||
onClose()
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
this.setState({isUploading: false})
|
||||
console.error(error.data.error)
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +161,7 @@ class WriteDataForm extends Component {
|
|||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={el => this.fileInput = el}
|
||||
fileInput={el => (this.fileInput = el)}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
|
|
|
@ -8,7 +8,7 @@ const WriteDataHeader = ({
|
|||
toggleWriteView,
|
||||
isManual,
|
||||
onClose,
|
||||
}) => (
|
||||
}) =>
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
|
@ -36,7 +36,6 @@ const WriteDataHeader = ({
|
|||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {VIS_VIEWS} from 'shared/constants'
|
|||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from '../constants'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
import * as viewActions from 'src/data_explorer/actions/view'
|
||||
import * as dataExplorerActionCreators from 'src/data_explorer/actions/view'
|
||||
import {writeLineProtocolAsync} from 'src/data_explorer/actions/view/write'
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
@ -152,7 +152,7 @@ function mapStateToProps(state) {
|
|||
const {
|
||||
app: {persisted: {autoRefresh}},
|
||||
dataExplorer,
|
||||
queryConfigs,
|
||||
dataExplorerQueryConfigs: queryConfigs,
|
||||
timeRange,
|
||||
} = state
|
||||
const queryConfigValues = _.values(queryConfigs)
|
||||
|
@ -169,9 +169,15 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
||||
errorThrownAction: bindActionCreators(errorThrown, dispatch),
|
||||
setTimeRange: bindActionCreators(viewActions.setTimeRange, dispatch),
|
||||
setTimeRange: bindActionCreators(
|
||||
dataExplorerActionCreators.setTimeRange,
|
||||
dispatch
|
||||
),
|
||||
writeLineProtocol: bindActionCreators(writeLineProtocolAsync, dispatch),
|
||||
queryConfigActions: bindActionCreators(viewActions, dispatch),
|
||||
queryConfigActions: bindActionCreators(
|
||||
dataExplorerActionCreators,
|
||||
dispatch
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,9 +44,7 @@ const Header = React.createClass({
|
|||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Data Explorer
|
||||
</h1>
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import queryConfigs from './queryConfigs'
|
||||
import dataExplorerQueryConfigs from './queryConfigs'
|
||||
import timeRange from './timeRange'
|
||||
import dataExplorer from './ui'
|
||||
|
||||
export default {
|
||||
queryConfigs,
|
||||
dataExplorerQueryConfigs,
|
||||
timeRange,
|
||||
dataExplorer,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import {
|
||||
editRawText,
|
||||
|
@ -11,15 +13,10 @@ import {
|
|||
toggleTagAcceptance,
|
||||
updateRawQuery,
|
||||
} from 'src/utils/queryTransitions'
|
||||
import update from 'react-addons-update'
|
||||
|
||||
export default function queryConfigs(state = {}, action) {
|
||||
const queryConfigs = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_EXPLORER': {
|
||||
return action.payload.explorer.data.queryConfigs
|
||||
}
|
||||
|
||||
case 'CHOOSE_NAMESPACE': {
|
||||
case 'DE_CHOOSE_NAMESPACE': {
|
||||
const {queryId, database, retentionPolicy} = action.payload
|
||||
const nextQueryConfig = chooseNamespace(state[queryId], {
|
||||
database,
|
||||
|
@ -31,7 +28,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'CHOOSE_MEASUREMENT': {
|
||||
case 'DE_CHOOSE_MEASUREMENT': {
|
||||
const {queryId, measurement} = action.payload
|
||||
const nextQueryConfig = chooseMeasurement(state[queryId], measurement)
|
||||
|
||||
|
@ -42,17 +39,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'LOAD_KAPACITOR_QUERY': {
|
||||
const {query} = action.payload
|
||||
const nextState = Object.assign({}, state, {
|
||||
[query.id]: query,
|
||||
})
|
||||
|
||||
return nextState
|
||||
}
|
||||
|
||||
case 'ADD_KAPACITOR_QUERY':
|
||||
case 'ADD_QUERY': {
|
||||
case 'DE_ADD_QUERY': {
|
||||
const {queryID, options} = action.payload
|
||||
const nextState = Object.assign({}, state, {
|
||||
[queryID]: Object.assign({}, defaultQueryConfig(queryID), options),
|
||||
|
@ -61,21 +48,12 @@ export default function queryConfigs(state = {}, action) {
|
|||
return nextState
|
||||
}
|
||||
|
||||
case 'UPDATE_QUERY': {
|
||||
const {queryId, updates} = action.payload
|
||||
const nextState = update(state, {
|
||||
[queryId]: {$merge: updates},
|
||||
})
|
||||
|
||||
return nextState
|
||||
}
|
||||
|
||||
case 'UPDATE_QUERY_CONFIG': {
|
||||
case 'DE_UPDATE_QUERY_CONFIG': {
|
||||
const {config} = action.payload
|
||||
return {...state, [config.id]: config}
|
||||
}
|
||||
|
||||
case 'EDIT_RAW_TEXT': {
|
||||
case 'DE_EDIT_RAW_TEXT': {
|
||||
const {queryId, rawText} = action.payload
|
||||
const nextQueryConfig = editRawText(state[queryId], rawText)
|
||||
|
||||
|
@ -84,7 +62,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'GROUP_BY_TIME': {
|
||||
case 'DE_GROUP_BY_TIME': {
|
||||
const {queryId, time} = action.payload
|
||||
const nextQueryConfig = groupByTime(state[queryId], time)
|
||||
|
||||
|
@ -93,7 +71,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'TOGGLE_TAG_ACCEPTANCE': {
|
||||
case 'DE_TOGGLE_TAG_ACCEPTANCE': {
|
||||
const {queryId} = action.payload
|
||||
const nextQueryConfig = toggleTagAcceptance(state[queryId])
|
||||
|
||||
|
@ -102,42 +80,30 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'DELETE_QUERY': {
|
||||
case 'DE_DELETE_QUERY': {
|
||||
const {queryID} = action.payload
|
||||
const nextState = update(state, {
|
||||
$apply: configs => {
|
||||
delete configs[queryID]
|
||||
return configs
|
||||
},
|
||||
})
|
||||
|
||||
return nextState
|
||||
return _.omit(state, queryID)
|
||||
}
|
||||
|
||||
case 'TOGGLE_FIELD': {
|
||||
const {isKapacitorRule} = action.meta
|
||||
case 'DE_TOGGLE_FIELD': {
|
||||
const {queryId, fieldFunc} = action.payload
|
||||
const nextQueryConfig = toggleField(
|
||||
state[queryId],
|
||||
fieldFunc,
|
||||
isKapacitorRule
|
||||
)
|
||||
const nextQueryConfig = toggleField(state[queryId], fieldFunc)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: {...nextQueryConfig, rawText: null},
|
||||
})
|
||||
}
|
||||
|
||||
case 'APPLY_FUNCS_TO_FIELD': {
|
||||
const {queryId, fieldFunc, isInDataExplorer} = action.payload
|
||||
const nextQueryConfig = applyFuncsToField(state[queryId], fieldFunc, isInDataExplorer)
|
||||
case 'DE_APPLY_FUNCS_TO_FIELD': {
|
||||
const {queryId, fieldFunc} = action.payload
|
||||
const nextQueryConfig = applyFuncsToField(state[queryId], fieldFunc, true)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
case 'CHOOSE_TAG': {
|
||||
case 'DE_CHOOSE_TAG': {
|
||||
const {queryId, tag} = action.payload
|
||||
const nextQueryConfig = chooseTag(state[queryId], tag)
|
||||
|
||||
|
@ -146,7 +112,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'GROUP_BY_TAG': {
|
||||
case 'DE_GROUP_BY_TAG': {
|
||||
const {queryId, tagKey} = action.payload
|
||||
const nextQueryConfig = groupByTag(state[queryId], tagKey)
|
||||
return Object.assign({}, state, {
|
||||
|
@ -154,7 +120,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'UPDATE_RAW_QUERY': {
|
||||
case 'DE_UPDATE_RAW_QUERY': {
|
||||
const {queryID, text} = action.payload
|
||||
const nextQueryConfig = updateRawQuery(state[queryID], text)
|
||||
return Object.assign({}, state, {
|
||||
|
@ -162,7 +128,7 @@ export default function queryConfigs(state = {}, action) {
|
|||
})
|
||||
}
|
||||
|
||||
case 'EDIT_QUERY_STATUS': {
|
||||
case 'DE_EDIT_QUERY_STATUS': {
|
||||
const {queryID, status} = action.payload
|
||||
const nextState = {
|
||||
[queryID]: {...state[queryID], status},
|
||||
|
@ -173,3 +139,5 @@ export default function queryConfigs(state = {}, action) {
|
|||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export default queryConfigs
|
||||
|
|
|
@ -9,7 +9,7 @@ const initialState = {
|
|||
|
||||
export default function timeRange(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'SET_TIME_RANGE': {
|
||||
case 'DE_SET_TIME_RANGE': {
|
||||
const {bounds} = action.payload
|
||||
|
||||
return {...state, ...bounds}
|
||||
|
|
|
@ -4,7 +4,7 @@ const initialState = {
|
|||
|
||||
export default function ui(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'ADD_QUERY': {
|
||||
case 'DE_ADD_QUERY': {
|
||||
const {queryID} = action.payload
|
||||
const newState = {
|
||||
queryIDs: state.queryIDs.concat(queryID),
|
||||
|
@ -13,7 +13,7 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'DELETE_QUERY': {
|
||||
case 'DE_DELETE_QUERY': {
|
||||
const {queryID} = action.payload
|
||||
const newState = {
|
||||
queryIDs: state.queryIDs.filter(id => id !== queryID),
|
||||
|
|
|
@ -73,9 +73,8 @@ const HostsTable = React.createClass({
|
|||
updateSort(key) {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
if (this.state.sortKey === key) {
|
||||
const reverseDirection = this.state.sortDirection === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
const reverseDirection =
|
||||
this.state.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
this.setState({sortDirection: reverseDirection})
|
||||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
|
@ -118,7 +117,9 @@ const HostsTable = React.createClass({
|
|||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{hostsTitle}</h2>
|
||||
<h2 className="panel-title">
|
||||
{hostsTitle}
|
||||
</h2>
|
||||
<SearchBar onSearch={this.updateSearchTerm} />
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
|
@ -165,9 +166,7 @@ const HostsTable = React.createClass({
|
|||
</tbody>
|
||||
</table>
|
||||
: <div className="generic-empty-state">
|
||||
<h4 style={{margin: '90px 0'}}>
|
||||
No Hosts found
|
||||
</h4>
|
||||
<h4 style={{margin: '90px 0'}}>No Hosts found</h4>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -202,7 +201,9 @@ const HostRow = React.createClass({
|
|||
return (
|
||||
<tr>
|
||||
<td style={{width: colName}}>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{width: colStatus}}>
|
||||
<div
|
||||
|
|
|
@ -98,9 +98,13 @@ export const HostPage = React.createClass({
|
|||
this.setState({layouts: filteredLayouts, hosts: filteredHosts}) // eslint-disable-line react/no-did-mount-set-state
|
||||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find(range => range.lower === lower)
|
||||
this.setState({timeRange})
|
||||
handleChooseTimeRange({lower, upper}) {
|
||||
if (upper) {
|
||||
this.setState({timeRange: {lower, upper}})
|
||||
} else {
|
||||
const timeRange = timeRanges.find(range => range.lower === lower)
|
||||
this.setState({timeRange})
|
||||
}
|
||||
},
|
||||
|
||||
synchronizer(dygraph) {
|
||||
|
|
|
@ -81,9 +81,7 @@ export const HostsPage = React.createClass({
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Host List
|
||||
</h1>
|
||||
<h1 className="page-header__title">Host List</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({
|
||||
type: 'KAPA_CHOOSE_NAMESPACE',
|
||||
payload: {
|
||||
queryId,
|
||||
database,
|
||||
retentionPolicy,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseMeasurement = (queryId, measurement) => ({
|
||||
type: 'KAPA_CHOOSE_MEASUREMENT',
|
||||
payload: {
|
||||
queryId,
|
||||
measurement,
|
||||
},
|
||||
})
|
||||
|
||||
export const chooseTag = (queryId, tag) => ({
|
||||
type: 'KAPA_CHOOSE_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tag,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTag = (queryId, tagKey) => ({
|
||||
type: 'KAPA_GROUP_BY_TAG',
|
||||
payload: {
|
||||
queryId,
|
||||
tagKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleTagAcceptance = queryId => ({
|
||||
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE',
|
||||
payload: {
|
||||
queryId,
|
||||
},
|
||||
})
|
||||
|
||||
export const toggleField = (queryId, fieldFunc) => ({
|
||||
type: 'KAPA_TOGGLE_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const applyFuncsToField = (queryId, fieldFunc) => ({
|
||||
type: 'KAPA_APPLY_FUNCS_TO_FIELD',
|
||||
payload: {
|
||||
queryId,
|
||||
fieldFunc,
|
||||
},
|
||||
})
|
||||
|
||||
export const groupByTime = (queryId, time) => ({
|
||||
type: 'KAPA_GROUP_BY_TIME',
|
||||
payload: {
|
||||
queryId,
|
||||
time,
|
||||
},
|
||||
})
|
|
@ -7,6 +7,14 @@ import {
|
|||
deleteRule as deleteRuleAPI,
|
||||
updateRuleStatus as updateRuleStatusAPI,
|
||||
} from 'src/kapacitor/apis'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
const loadQuery = query => ({
|
||||
type: 'KAPA_LOAD_QUERY',
|
||||
payload: {
|
||||
query,
|
||||
},
|
||||
})
|
||||
|
||||
export function fetchRule(source, ruleID) {
|
||||
return dispatch => {
|
||||
|
@ -18,18 +26,19 @@ export function fetchRule(source, ruleID) {
|
|||
rule: Object.assign(rule, {queryID: rule.query.id}),
|
||||
},
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'LOAD_KAPACITOR_QUERY',
|
||||
payload: {
|
||||
query: rule.query,
|
||||
},
|
||||
})
|
||||
dispatch(loadQuery(rule.query))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const addQuery = queryID => ({
|
||||
type: 'KAPA_ADD_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export function loadDefaultRule() {
|
||||
return dispatch => {
|
||||
const queryID = uuid.v4()
|
||||
|
@ -39,25 +48,16 @@ export function loadDefaultRule() {
|
|||
queryID,
|
||||
},
|
||||
})
|
||||
dispatch({
|
||||
type: 'ADD_KAPACITOR_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
dispatch(addQuery(queryID))
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchRules(kapacitor) {
|
||||
return dispatch => {
|
||||
getRules(kapacitor).then(({data: {rules}}) => {
|
||||
dispatch({
|
||||
type: 'LOAD_RULES',
|
||||
payload: {
|
||||
rules,
|
||||
},
|
||||
})
|
||||
})
|
||||
export const fetchRules = kapacitor => async dispatch => {
|
||||
try {
|
||||
const {data: {rules}} = await getRules(kapacitor)
|
||||
dispatch({type: 'LOAD_RULES', payload: {rules}})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,15 @@ export function updateDetails(ruleID, details) {
|
|||
}
|
||||
}
|
||||
|
||||
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
|
||||
type: 'UPDATE_RULE_ALERT_PROPERTY',
|
||||
payload: {
|
||||
ruleID,
|
||||
alertNodeName,
|
||||
alertProperty,
|
||||
},
|
||||
})
|
||||
|
||||
export function updateAlerts(ruleID, alerts) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_ALERTS',
|
||||
|
@ -127,12 +136,12 @@ export function updateAlerts(ruleID, alerts) {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateAlertNodes(ruleID, alertType, alertNodesText) {
|
||||
export function updateAlertNodes(ruleID, alertNodeName, alertNodesText) {
|
||||
return {
|
||||
type: 'UPDATE_RULE_ALERT_NODES',
|
||||
payload: {
|
||||
ruleID,
|
||||
alertType,
|
||||
alertNodeName,
|
||||
alertNodesText,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
HipChatConfig,
|
||||
OpsGenieConfig,
|
||||
PagerDutyConfig,
|
||||
PushoverConfig,
|
||||
SensuConfig,
|
||||
SlackConfig,
|
||||
SMTPConfig,
|
||||
|
@ -124,99 +125,97 @@ class AlertTabs extends Component {
|
|||
this.handleTest('slack', properties)
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
const supportedConfigs = {
|
||||
alerta: {
|
||||
type: 'Alerta',
|
||||
component: (
|
||||
renderComponent: () =>
|
||||
<AlertaConfig
|
||||
onSave={p => this.handleSaveConfig('alerta', p)}
|
||||
config={this.getSection(configSections, 'alerta')}
|
||||
/>
|
||||
),
|
||||
/>,
|
||||
},
|
||||
{
|
||||
type: 'SMTP',
|
||||
component: (
|
||||
<SMTPConfig
|
||||
onSave={p => this.handleSaveConfig('smtp', p)}
|
||||
config={this.getSection(configSections, 'smtp')}
|
||||
/>
|
||||
),
|
||||
hipchat: {
|
||||
type: 'HipChat',
|
||||
renderComponent: () =>
|
||||
<HipChatConfig
|
||||
onSave={p => this.handleSaveConfig('hipchat', p)}
|
||||
config={this.getSection(configSections, 'hipchat')}
|
||||
/>,
|
||||
},
|
||||
{
|
||||
opsgenie: {
|
||||
type: 'OpsGenie',
|
||||
renderComponent: () =>
|
||||
<OpsGenieConfig
|
||||
onSave={p => this.handleSaveConfig('opsgenie', p)}
|
||||
config={this.getSection(configSections, 'opsgenie')}
|
||||
/>,
|
||||
},
|
||||
pagerduty: {
|
||||
type: 'PagerDuty',
|
||||
renderComponent: () =>
|
||||
<PagerDutyConfig
|
||||
onSave={p => this.handleSaveConfig('pagerduty', p)}
|
||||
config={this.getSection(configSections, 'pagerduty')}
|
||||
/>,
|
||||
},
|
||||
pushover: {
|
||||
type: 'Pushover',
|
||||
renderComponent: () =>
|
||||
<PushoverConfig
|
||||
onSave={p => this.handleSaveConfig('pushover', p)}
|
||||
config={this.getSection(configSections, 'pushover')}
|
||||
/>,
|
||||
},
|
||||
sensu: {
|
||||
type: 'Sensu',
|
||||
renderComponent: () =>
|
||||
<SensuConfig
|
||||
onSave={p => this.handleSaveConfig('sensu', p)}
|
||||
config={this.getSection(configSections, 'sensu')}
|
||||
/>,
|
||||
},
|
||||
slack: {
|
||||
type: 'Slack',
|
||||
component: (
|
||||
renderComponent: () =>
|
||||
<SlackConfig
|
||||
onSave={p => this.handleSaveConfig('slack', p)}
|
||||
onTest={test}
|
||||
config={this.getSection(configSections, 'slack')}
|
||||
/>
|
||||
),
|
||||
/>,
|
||||
},
|
||||
{
|
||||
type: 'VictorOps',
|
||||
component: (
|
||||
<VictorOpsConfig
|
||||
onSave={p => this.handleSaveConfig('victorops', p)}
|
||||
config={this.getSection(configSections, 'victorops')}
|
||||
/>
|
||||
),
|
||||
smtp: {
|
||||
type: 'SMTP',
|
||||
renderComponent: () =>
|
||||
<SMTPConfig
|
||||
onSave={p => this.handleSaveConfig('smtp', p)}
|
||||
config={this.getSection(configSections, 'smtp')}
|
||||
/>,
|
||||
},
|
||||
{
|
||||
type: 'Telegram',
|
||||
component: (
|
||||
<TelegramConfig
|
||||
onSave={p => this.handleSaveConfig('telegram', p)}
|
||||
config={this.getSection(configSections, 'telegram')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'OpsGenie',
|
||||
component: (
|
||||
<OpsGenieConfig
|
||||
onSave={p => this.handleSaveConfig('opsgenie', p)}
|
||||
config={this.getSection(configSections, 'opsgenie')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'PagerDuty',
|
||||
component: (
|
||||
<PagerDutyConfig
|
||||
onSave={p => this.handleSaveConfig('pagerduty', p)}
|
||||
config={this.getSection(configSections, 'pagerduty')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'HipChat',
|
||||
component: (
|
||||
<HipChatConfig
|
||||
onSave={p => this.handleSaveConfig('hipchat', p)}
|
||||
config={this.getSection(configSections, 'hipchat')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'Sensu',
|
||||
component: (
|
||||
<SensuConfig
|
||||
onSave={p => this.handleSaveConfig('sensu', p)}
|
||||
config={this.getSection(configSections, 'sensu')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
talk: {
|
||||
type: 'Talk',
|
||||
component: (
|
||||
renderComponent: () =>
|
||||
<TalkConfig
|
||||
onSave={p => this.handleSaveConfig('talk', p)}
|
||||
config={this.getSection(configSections, 'talk')}
|
||||
/>
|
||||
),
|
||||
/>,
|
||||
},
|
||||
]
|
||||
telegram: {
|
||||
type: 'Telegram',
|
||||
renderComponent: () =>
|
||||
<TelegramConfig
|
||||
onSave={p => this.handleSaveConfig('telegram', p)}
|
||||
config={this.getSection(configSections, 'telegram')}
|
||||
/>,
|
||||
},
|
||||
victorops: {
|
||||
type: 'VictorOps',
|
||||
renderComponent: () =>
|
||||
<VictorOpsConfig
|
||||
onSave={p => this.handleSaveConfig('victorops', p)}
|
||||
config={this.getSection(configSections, 'victorops')}
|
||||
/>,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -228,11 +227,31 @@ class AlertTabs extends Component {
|
|||
|
||||
<Tabs tabContentsClass="config-endpoint">
|
||||
<TabList customClass="config-endpoint--tabs">
|
||||
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
|
||||
{_.reduce(
|
||||
configSections,
|
||||
(acc, _cur, k) =>
|
||||
supportedConfigs[k]
|
||||
? acc.concat(
|
||||
<Tab key={supportedConfigs[k].type}>
|
||||
{supportedConfigs[k].type}
|
||||
</Tab>
|
||||
)
|
||||
: acc,
|
||||
[]
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanels customClass="config-endpoint--tab-contents">
|
||||
{tabs.map((t, i) =>
|
||||
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
|
||||
{_.reduce(
|
||||
configSections,
|
||||
(acc, _cur, k) =>
|
||||
supportedConfigs[k]
|
||||
? acc.concat(
|
||||
<TabPanel key={supportedConfigs[k].type}>
|
||||
{supportedConfigs[k].renderComponent()}
|
||||
</TabPanel>
|
||||
)
|
||||
: acc,
|
||||
[]
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const CodeData = ({onClickTemplate, template}) =>
|
||||
<code
|
||||
className="rule-builder--message-template"
|
||||
data-tip={template.text}
|
||||
onClick={onClickTemplate}
|
||||
>
|
||||
{template.label}
|
||||
</code>
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
CodeData.propTypes = {
|
||||
onClickTemplate: func,
|
||||
template: shape({
|
||||
label: string,
|
||||
text: string,
|
||||
}),
|
||||
}
|
||||
|
||||
export default CodeData
|
|
@ -32,6 +32,7 @@ export const DataSection = React.createClass({
|
|||
onAddEvery: PropTypes.func.isRequired,
|
||||
onRemoveEvery: PropTypes.func.isRequired,
|
||||
timeRange: PropTypes.shape({}).isRequired,
|
||||
isKapacitorRule: PropTypes.bool,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
|
@ -56,7 +57,7 @@ export const DataSection = React.createClass({
|
|||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
this.props.actions.toggleField(this.props.query.id, field, true)
|
||||
this.props.actions.toggleField(this.props.query.id, field)
|
||||
// Every is only added when a function has been added to a field.
|
||||
// Here, the field is selected without a function.
|
||||
this.props.onRemoveEvery()
|
||||
|
@ -109,7 +110,7 @@ export const DataSection = React.createClass({
|
|||
},
|
||||
|
||||
renderQueryBuilder() {
|
||||
const {query} = this.props
|
||||
const {query, isKapacitorRule} = this.props
|
||||
|
||||
return (
|
||||
<div className="query-builder">
|
||||
|
@ -129,7 +130,7 @@ export const DataSection = React.createClass({
|
|||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
isKapacitorRule={true}
|
||||
isKapacitorRule={isKapacitorRule}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -13,9 +13,7 @@ class KapacitorForm extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Configure Kapacitor
|
||||
</h1>
|
||||
<h1 className="page-header__title">Configure Kapacitor</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,8 +17,8 @@ export const KapacitorRule = React.createClass({
|
|||
rule: PropTypes.shape({}).isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}).isRequired,
|
||||
queryActions: PropTypes.shape({}).isRequired,
|
||||
kapacitorActions: PropTypes.shape({}).isRequired,
|
||||
queryConfigActions: PropTypes.shape({}).isRequired,
|
||||
ruleActions: PropTypes.shape({}).isRequired,
|
||||
addFlashMessage: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool.isRequired,
|
||||
enabledAlerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
|
@ -36,23 +36,23 @@ export const KapacitorRule = React.createClass({
|
|||
|
||||
render() {
|
||||
const {
|
||||
queryActions,
|
||||
queryConfigActions,
|
||||
source,
|
||||
enabledAlerts,
|
||||
queryConfigs,
|
||||
query,
|
||||
rule,
|
||||
kapacitorActions,
|
||||
ruleActions,
|
||||
isEditing,
|
||||
} = this.props
|
||||
const {chooseTrigger, updateRuleValues} = kapacitorActions
|
||||
const {chooseTrigger, updateRuleValues} = ruleActions
|
||||
const {timeRange} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<RuleHeader
|
||||
rule={rule}
|
||||
actions={kapacitorActions}
|
||||
actions={ruleActions}
|
||||
onSave={isEditing ? this.handleEdit : this.handleCreate}
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
validationError={this.validationError()}
|
||||
|
@ -68,9 +68,10 @@ export const KapacitorRule = React.createClass({
|
|||
timeRange={timeRange}
|
||||
source={source}
|
||||
query={query}
|
||||
actions={queryActions}
|
||||
actions={queryConfigActions}
|
||||
onAddEvery={this.handleAddEvery}
|
||||
onRemoveEvery={this.handleRemoveEvery}
|
||||
isKapacitorRule={true}
|
||||
/>
|
||||
<ValuesSection
|
||||
rule={rule}
|
||||
|
@ -86,7 +87,7 @@ export const KapacitorRule = React.createClass({
|
|||
/>
|
||||
<RuleMessage
|
||||
rule={rule}
|
||||
actions={kapacitorActions}
|
||||
actions={ruleActions}
|
||||
enabledAlerts={enabledAlerts}
|
||||
/>
|
||||
</div>
|
||||
|
@ -133,7 +134,6 @@ export const KapacitorRule = React.createClass({
|
|||
|
||||
handleEdit() {
|
||||
const {addFlashMessage, queryConfigs, rule} = this.props
|
||||
|
||||
const updatedRule = Object.assign({}, rule, {
|
||||
query: queryConfigs[rule.queryID],
|
||||
})
|
||||
|
@ -151,12 +151,12 @@ export const KapacitorRule = React.createClass({
|
|||
},
|
||||
|
||||
handleAddEvery(frequency) {
|
||||
const {rule: {id: ruleID}, kapacitorActions: {addEvery}} = this.props
|
||||
const {rule: {id: ruleID}, ruleActions: {addEvery}} = this.props
|
||||
addEvery(ruleID, frequency)
|
||||
},
|
||||
|
||||
handleRemoveEvery() {
|
||||
const {rule: {id: ruleID}, kapacitorActions: {removeEvery}} = this.props
|
||||
const {rule: {id: ruleID}, ruleActions: {removeEvery}} = this.props
|
||||
removeEvery(ruleID)
|
||||
},
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import NoKapacitorError from 'shared/components/NoKapacitorError'
|
|||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import TICKscriptOverlay from 'src/kapacitor/components/TICKscriptOverlay'
|
||||
|
||||
const KapacitorRules = ({
|
||||
source,
|
||||
|
@ -12,8 +13,29 @@ const KapacitorRules = ({
|
|||
hasKapacitor,
|
||||
loading,
|
||||
onDelete,
|
||||
tickscript,
|
||||
onChangeRuleStatus,
|
||||
onReadTickscript,
|
||||
onCloseTickscript,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContents>
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Alert Rules</h2>
|
||||
<button className="btn btn-primary btn-sm disabled" disabled={true}>
|
||||
Create Rule
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="generic-empty-state">
|
||||
<p>Loading Rules...</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageContents>
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasKapacitor) {
|
||||
return (
|
||||
<PageContents>
|
||||
|
@ -22,20 +44,19 @@ const KapacitorRules = ({
|
|||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContents>
|
||||
<h2>Loading...</h2>
|
||||
</PageContents>
|
||||
)
|
||||
}
|
||||
const tableHeader = rules.length === 1
|
||||
? '1 Alert Rule'
|
||||
: `${rules.length} Alert Rules`
|
||||
const tableHeader =
|
||||
rules.length === 1 ? '1 Alert Rule' : `${rules.length} Alert Rules`
|
||||
return (
|
||||
<PageContents source={source}>
|
||||
<PageContents
|
||||
source={source}
|
||||
tickscript={tickscript}
|
||||
onReadTickscript={onReadTickscript}
|
||||
onCloseTickscript={onCloseTickscript}
|
||||
>
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{tableHeader}</h2>
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<Link
|
||||
to={`/sources/${source.id}/alert-rules/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
|
@ -47,13 +68,14 @@ const KapacitorRules = ({
|
|||
source={source}
|
||||
rules={rules}
|
||||
onDelete={onDelete}
|
||||
onReadTickscript={onReadTickscript}
|
||||
onChangeRuleStatus={onChangeRuleStatus}
|
||||
/>
|
||||
</PageContents>
|
||||
)
|
||||
}
|
||||
|
||||
const PageContents = ({children, source}) =>
|
||||
const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
|
@ -76,9 +98,15 @@ const PageContents = ({children, source}) =>
|
|||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
{tickscript
|
||||
? <TICKscriptOverlay
|
||||
tickscript={tickscript}
|
||||
onClose={onCloseTickscript}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
const {arrayOf, bool, func, shape, node} = PropTypes
|
||||
const {arrayOf, bool, func, node, shape, string} = PropTypes
|
||||
|
||||
KapacitorRules.propTypes = {
|
||||
source: shape(),
|
||||
|
@ -87,11 +115,16 @@ KapacitorRules.propTypes = {
|
|||
loading: bool,
|
||||
onChangeRuleStatus: func,
|
||||
onDelete: func,
|
||||
tickscript: string,
|
||||
onReadTickscript: func,
|
||||
onCloseTickscript: func,
|
||||
}
|
||||
|
||||
PageContents.propTypes = {
|
||||
children: node,
|
||||
source: shape(),
|
||||
tickscript: string,
|
||||
onCloseTickscript: func,
|
||||
}
|
||||
|
||||
export default KapacitorRules
|
||||
|
|
|
@ -1,21 +1,40 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
|
||||
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
|
||||
const {
|
||||
colName,
|
||||
colType,
|
||||
colMessage,
|
||||
colAlerts,
|
||||
colEnabled,
|
||||
colActions,
|
||||
} = KAPACITOR_RULES_TABLE
|
||||
|
||||
const KapacitorRulesTable = ({
|
||||
rules,
|
||||
source,
|
||||
onDelete,
|
||||
onReadTickscript,
|
||||
onChangeRuleStatus,
|
||||
}) =>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Rule Type</th>
|
||||
<th>Message</th>
|
||||
<th>Alerts</th>
|
||||
<th className="text-center">Enabled</th>
|
||||
<th />
|
||||
<th style={{width: colName}}>Name</th>
|
||||
<th style={{width: colType}}>Rule Type</th>
|
||||
<th style={{width: colMessage}}>Message</th>
|
||||
<th style={{width: colAlerts}}>Alerts</th>
|
||||
<th style={{width: colEnabled}} className="text-center">
|
||||
Enabled
|
||||
</th>
|
||||
<th style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rules.map(rule => {
|
||||
{_.sortBy(rules, r => r.name.toLowerCase()).map(rule => {
|
||||
return (
|
||||
<RuleRow
|
||||
key={rule.id}
|
||||
|
@ -23,6 +42,7 @@ const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
|
|||
source={source}
|
||||
onDelete={onDelete}
|
||||
onChangeRuleStatus={onChangeRuleStatus}
|
||||
onRead={onReadTickscript}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -30,15 +50,26 @@ const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
|
|||
</table>
|
||||
</div>
|
||||
|
||||
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
||||
const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
|
||||
<tr key={rule.id}>
|
||||
<td className="monotype">
|
||||
<td style={{width: colName}} className="monotype">
|
||||
<RuleTitle rule={rule} source={source} />
|
||||
</td>
|
||||
<td className="monotype">{rule.trigger}</td>
|
||||
<td className="monotype">{rule.message}</td>
|
||||
<td className="monotype">{rule.alerts.join(', ')}</td>
|
||||
<td className="monotype text-center">
|
||||
<td style={{width: colType}} className="monotype">
|
||||
{rule.trigger}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<span
|
||||
className="table-cell-nowrap"
|
||||
style={{display: 'inline-block', maxWidth: colMessage}}
|
||||
>
|
||||
{rule.message}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{width: colAlerts}} className="monotype">
|
||||
{rule.alerts.join(', ')}
|
||||
</td>
|
||||
<td style={{width: colEnabled}} className="monotype text-center">
|
||||
<div className="dark-checkbox">
|
||||
<input
|
||||
id={`kapacitor-enabled ${rule.id}`}
|
||||
|
@ -50,7 +81,10 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
|||
<label htmlFor={`kapacitor-enabled ${rule.id}`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<td style={{width: colActions}} className="text-right table-cell-nowrap">
|
||||
<button className="btn btn-info btn-xs" onClick={() => onRead(rule)}>
|
||||
View TICKscript
|
||||
</button>
|
||||
<button className="btn btn-danger btn-xs" onClick={() => onDelete(rule)}>
|
||||
Delete
|
||||
</button>
|
||||
|
@ -60,10 +94,18 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
|||
const RuleTitle = ({rule: {id, name, query}, source}) => {
|
||||
// no queryConfig means the rule was manually created outside of Chronograf
|
||||
if (!query) {
|
||||
return <i>{name}</i>
|
||||
return (
|
||||
<i>
|
||||
{name}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return <Link to={`/sources/${source.id}/alert-rules/${id}`}>{name}</Link>
|
||||
return (
|
||||
<Link to={`/sources/${source.id}/alert-rules/${id}`}>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
@ -75,6 +117,7 @@ KapacitorRulesTable.propTypes = {
|
|||
source: shape({
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
onReadTickscript: func,
|
||||
}
|
||||
|
||||
RuleRow.propTypes = {
|
||||
|
@ -82,6 +125,7 @@ RuleRow.propTypes = {
|
|||
source: shape(),
|
||||
onChangeRuleStatus: func,
|
||||
onDelete: func,
|
||||
onRead: func,
|
||||
}
|
||||
|
||||
RuleTitle.propTypes = {
|
||||
|
@ -90,7 +134,7 @@ RuleTitle.propTypes = {
|
|||
query: shape(),
|
||||
links: shape({
|
||||
self: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
}),
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
|
|
|
@ -34,7 +34,9 @@ export const RuleGraph = React.createClass({
|
|||
if (!queryText) {
|
||||
return (
|
||||
<div className="rule-builder--graph-empty">
|
||||
<p>Select a <strong>Time-Series</strong> to preview on a graph</p>
|
||||
<p>
|
||||
Select a <strong>Time-Series</strong> to preview on a graph
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -106,9 +108,10 @@ export const RuleGraph = React.createClass({
|
|||
const bottom = dygraph.toDomYCoord(highlightStart)
|
||||
const top = dygraph.toDomYCoord(highlightEnd)
|
||||
|
||||
canvas.fillStyle = rule.values.operator === 'outside range'
|
||||
? 'rgba(41, 41, 51, 1)'
|
||||
: 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillStyle =
|
||||
rule.values.operator === 'outside range'
|
||||
? 'rgba(41, 41, 51, 1)'
|
||||
: 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillRect(area.x, top, area.w, bottom - top)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -83,6 +83,7 @@ export const RuleHeader = React.createClass({
|
|||
<TimeRangeDropdown
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
selected={timeRange}
|
||||
preventCustomTimeRange={true}
|
||||
/>
|
||||
{saveButton}
|
||||
<ReactTooltip
|
||||
|
|
|
@ -1,41 +1,36 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
import RuleMessageAlertConfig from 'src/kapacitor/components/RuleMessageAlertConfig'
|
||||
import RuleMessageOptions from 'src/kapacitor/components/RuleMessageOptions'
|
||||
import RuleMessageText from 'src/kapacitor/components/RuleMessageText'
|
||||
import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates'
|
||||
|
||||
import {RULE_MESSAGE_TEMPLATES as templates, DEFAULT_ALERTS} from '../constants'
|
||||
import {DEFAULT_ALERTS, RULE_ALERT_OPTIONS} from 'src/kapacitor/constants'
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
class RuleMessage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
export const RuleMessage = React.createClass({
|
||||
propTypes: {
|
||||
rule: shape({}).isRequired,
|
||||
actions: shape({
|
||||
updateMessage: func.isRequired,
|
||||
updateDetails: func.isRequired,
|
||||
}).isRequired,
|
||||
enabledAlerts: arrayOf(string.isRequired).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedAlert: null,
|
||||
selectedAlertProperty: null,
|
||||
this.state = {
|
||||
selectedAlertNodeName: null,
|
||||
}
|
||||
},
|
||||
|
||||
this.handleChangeMessage = ::this.handleChangeMessage
|
||||
this.handleChooseAlert = ::this.handleChooseAlert
|
||||
}
|
||||
|
||||
handleChangeMessage() {
|
||||
const {actions, rule} = this.props
|
||||
actions.updateMessage(rule.id, this.message.value)
|
||||
},
|
||||
}
|
||||
|
||||
handleChooseAlert(item) {
|
||||
const {actions} = this.props
|
||||
actions.updateAlerts(item.ruleID, [item.text])
|
||||
actions.updateAlertNodes(item.ruleID, item.text, '')
|
||||
this.setState({selectedAlert: item.text})
|
||||
},
|
||||
this.setState({selectedAlertNodeName: item.text})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {rule, actions, enabledAlerts} = this.props
|
||||
|
@ -43,13 +38,14 @@ export const RuleMessage = React.createClass({
|
|||
return {text, ruleID: rule.id}
|
||||
})
|
||||
|
||||
const alerts = enabledAlerts
|
||||
.map(text => {
|
||||
const alerts = [
|
||||
...defaultAlertEndpoints,
|
||||
...enabledAlerts.map(text => {
|
||||
return {text, ruleID: rule.id}
|
||||
})
|
||||
.concat(defaultAlertEndpoints)
|
||||
}),
|
||||
]
|
||||
|
||||
const selectedAlert = rule.alerts[0] || alerts[0].text
|
||||
const selectedAlertNodeName = rule.alerts[0] || alerts[0].text
|
||||
|
||||
return (
|
||||
<div className="rule-section">
|
||||
|
@ -58,96 +54,51 @@ export const RuleMessage = React.createClass({
|
|||
<div className="rule-section--row rule-section--row-first rule-section--border-bottom">
|
||||
<p>Send this Alert to:</p>
|
||||
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite">
|
||||
{alerts.map(alert =>
|
||||
<li
|
||||
key={alert.text}
|
||||
className={classnames({
|
||||
active: alert.text === selectedAlert,
|
||||
})}
|
||||
onClick={() => this.handleChooseAlert(alert)}
|
||||
>
|
||||
{alert.text}
|
||||
</li>
|
||||
)}
|
||||
{alerts
|
||||
// only display alert endpoints that have rule alert options configured
|
||||
.filter(alert => _.get(RULE_ALERT_OPTIONS, alert.text, false))
|
||||
.map(alert =>
|
||||
<li
|
||||
key={alert.text}
|
||||
className={classnames({
|
||||
active: alert.text === selectedAlertNodeName,
|
||||
})}
|
||||
onClick={() => this.handleChooseAlert(alert)}
|
||||
>
|
||||
{alert.text}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<RuleMessageAlertConfig
|
||||
updateAlertNodes={actions.updateAlertNodes}
|
||||
alert={selectedAlert}
|
||||
<RuleMessageOptions
|
||||
rule={rule}
|
||||
alertNodeName={selectedAlertNodeName}
|
||||
updateAlertNodes={actions.updateAlertNodes}
|
||||
updateDetails={actions.updateDetails}
|
||||
updateAlertProperty={actions.updateAlertProperty}
|
||||
/>
|
||||
{selectedAlert === 'smtp'
|
||||
? <div className="rule-section--border-bottom">
|
||||
<textarea
|
||||
className="form-control form-malachite monotype rule-builder--message"
|
||||
placeholder="Email body text goes here"
|
||||
ref={r => (this.details = r)}
|
||||
onChange={() =>
|
||||
actions.updateDetails(rule.id, this.details.value)}
|
||||
value={rule.details}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
<textarea
|
||||
className="form-control form-malachite monotype rule-builder--message"
|
||||
ref={r => (this.message = r)}
|
||||
onChange={() => actions.updateMessage(rule.id, this.message.value)}
|
||||
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}"
|
||||
value={rule.message}
|
||||
spellCheck={false}
|
||||
<RuleMessageText rule={rule} updateMessage={actions.updateMessage} />
|
||||
<RuleMessageTemplates
|
||||
rule={rule}
|
||||
updateMessage={actions.updateMessage}
|
||||
/>
|
||||
<div className="rule-section--row rule-section--row-last rule-section--border-top">
|
||||
<p>Templates:</p>
|
||||
{Object.keys(templates).map(t => {
|
||||
return (
|
||||
<CodeData
|
||||
key={t}
|
||||
template={templates[t]}
|
||||
onClickTemplate={() =>
|
||||
actions.updateMessage(
|
||||
rule.id,
|
||||
`${this.message.value} ${templates[t].label}`
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<ReactTooltip
|
||||
effect="solid"
|
||||
html={true}
|
||||
offset={{top: -4}}
|
||||
class="influx-tooltip kapacitor-tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const CodeData = React.createClass({
|
||||
propTypes: {
|
||||
onClickTemplate: func,
|
||||
template: shape({
|
||||
label: string,
|
||||
text: string,
|
||||
}),
|
||||
},
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
render() {
|
||||
const {onClickTemplate, template} = this.props
|
||||
const {label, text} = template
|
||||
|
||||
return (
|
||||
<code
|
||||
className="rule-builder--message-template"
|
||||
data-tip={text}
|
||||
onClick={onClickTemplate}
|
||||
>
|
||||
{label}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
})
|
||||
RuleMessage.propTypes = {
|
||||
rule: shape({}).isRequired,
|
||||
actions: shape({
|
||||
updateAlertNodes: func.isRequired,
|
||||
updateMessage: func.isRequired,
|
||||
updateDetails: func.isRequired,
|
||||
updateAlertProperty: func.isRequired,
|
||||
}).isRequired,
|
||||
enabledAlerts: arrayOf(string.isRequired).isRequired,
|
||||
}
|
||||
|
||||
export default RuleMessage
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import {
|
||||
DEFAULT_ALERT_PLACEHOLDERS,
|
||||
DEFAULT_ALERT_LABELS,
|
||||
ALERT_NODES_ACCESSORS,
|
||||
} from '../constants'
|
||||
|
||||
const RuleMessageAlertConfig = ({updateAlertNodes, alert, rule}) => {
|
||||
if (!Object.keys(DEFAULT_ALERT_PLACEHOLDERS).find(a => a === alert)) {
|
||||
return null
|
||||
}
|
||||
if (!Object.keys(DEFAULT_ALERT_LABELS).find(a => a === alert)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="rule-section--row rule-section--border-bottom">
|
||||
<p>{DEFAULT_ALERT_LABELS[alert]}</p>
|
||||
<input
|
||||
id="alert-input"
|
||||
className="form-control input-sm form-malachite"
|
||||
style={{flex: '1 0 0'}}
|
||||
type="text"
|
||||
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
|
||||
onChange={e => updateAlertNodes(rule.id, alert, e.target.value)}
|
||||
value={ALERT_NODES_ACCESSORS[alert](rule)}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
RuleMessageAlertConfig.propTypes = {
|
||||
updateAlertNodes: func.isRequired,
|
||||
alert: string,
|
||||
rule: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default RuleMessageAlertConfig
|
|
@ -0,0 +1,135 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import {
|
||||
RULE_ALERT_OPTIONS,
|
||||
ALERT_NODES_ACCESSORS,
|
||||
} from 'src/kapacitor/constants'
|
||||
|
||||
class RuleMessageOptions extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.getAlertPropertyValue = ::this.getAlertPropertyValue
|
||||
}
|
||||
|
||||
getAlertPropertyValue(properties, name) {
|
||||
if (properties) {
|
||||
const alertNodeProperty = properties.find(
|
||||
property => property.name === name
|
||||
)
|
||||
if (alertNodeProperty) {
|
||||
return alertNodeProperty.args
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rule,
|
||||
alertNodeName,
|
||||
updateAlertNodes,
|
||||
updateDetails,
|
||||
updateAlertProperty,
|
||||
} = this.props
|
||||
const {args, details, properties} = RULE_ALERT_OPTIONS[alertNodeName]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{args
|
||||
? <div className="rule-section--row rule-section--border-bottom">
|
||||
<p>
|
||||
{args.label}
|
||||
</p>
|
||||
<input
|
||||
id="alert-input"
|
||||
className="form-control input-sm form-malachite"
|
||||
style={{flex: '1 0 0'}}
|
||||
type="text"
|
||||
placeholder={args.placeholder}
|
||||
onChange={e =>
|
||||
updateAlertNodes(rule.id, alertNodeName, e.target.value)}
|
||||
value={ALERT_NODES_ACCESSORS[alertNodeName](rule)}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
{properties && properties.length
|
||||
? <div
|
||||
className="rule-section--row rule-section--border-bottom"
|
||||
style={{display: 'block'}}
|
||||
>
|
||||
<p>Optional Alert Parameters</p>
|
||||
<div style={{display: 'flex', flexWrap: 'wrap'}}>
|
||||
{properties.map(({name: propertyName, label, placeholder}) =>
|
||||
<div
|
||||
key={propertyName}
|
||||
style={{display: 'block', flex: '0 0 33.33%'}}
|
||||
>
|
||||
<label
|
||||
htmlFor={label}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{flex: '0 0 auto'}}>
|
||||
{label}
|
||||
</span>
|
||||
<input
|
||||
name={label}
|
||||
className="form-control input-sm form-malachite"
|
||||
style={{
|
||||
margin: '0 15px 0 5px',
|
||||
flex: '1 0 0',
|
||||
}}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
onChange={e =>
|
||||
updateAlertProperty(rule.id, alertNodeName, {
|
||||
name: propertyName,
|
||||
args: [e.target.value],
|
||||
})}
|
||||
value={this.getAlertPropertyValue(
|
||||
rule.alertNodes[0].properties,
|
||||
propertyName
|
||||
)}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
{details
|
||||
? <div className="rule-section--border-bottom">
|
||||
<textarea
|
||||
className="form-control form-malachite monotype rule-builder--message"
|
||||
placeholder={details.placeholder ? details.placeholder : ''}
|
||||
ref={r => (this.details = r)}
|
||||
onChange={() => updateDetails(rule.id, this.details.value)}
|
||||
value={rule.details}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
RuleMessageOptions.propTypes = {
|
||||
rule: shape({}).isRequired,
|
||||
alertNodeName: string,
|
||||
updateAlertNodes: func.isRequired,
|
||||
updateDetails: func.isRequired,
|
||||
updateAlertProperty: func.isRequired,
|
||||
}
|
||||
|
||||
export default RuleMessageOptions
|
|
@ -0,0 +1,49 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
import CodeData from 'src/kapacitor/components/CodeData'
|
||||
|
||||
import {RULE_MESSAGE_TEMPLATES} from 'src/kapacitor/constants'
|
||||
|
||||
// needs to be React Component for CodeData click handler to work
|
||||
class RuleMessageTemplates extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {rule, updateMessage} = this.props
|
||||
|
||||
return (
|
||||
<div className="rule-section--row rule-section--row-last rule-section--border-top">
|
||||
<p>Templates:</p>
|
||||
{_.map(RULE_MESSAGE_TEMPLATES, (template, key) => {
|
||||
return (
|
||||
<CodeData
|
||||
key={key}
|
||||
template={template}
|
||||
onClickTemplate={() =>
|
||||
updateMessage(rule.id, `${rule.message} ${template.label}`)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<ReactTooltip
|
||||
effect="solid"
|
||||
html={true}
|
||||
offset={{top: -4}}
|
||||
class="influx-tooltip kapacitor-tooltip"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape} = PropTypes
|
||||
|
||||
RuleMessageTemplates.propTypes = {
|
||||
rule: shape().isRequired,
|
||||
updateMessage: func.isRequired,
|
||||
}
|
||||
|
||||
export default RuleMessageTemplates
|
|
@ -0,0 +1,31 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
class RuleMessageText extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {rule, updateMessage} = this.props
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className="form-control form-malachite monotype rule-builder--message"
|
||||
ref={r => (this.message = r)}
|
||||
onChange={() => updateMessage(rule.id, this.message.value)}
|
||||
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}"
|
||||
value={rule.message}
|
||||
spellCheck={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape} = PropTypes
|
||||
|
||||
RuleMessageText.propTypes = {
|
||||
rule: shape().isRequired,
|
||||
updateMessage: func.isRequired,
|
||||
}
|
||||
|
||||
export default RuleMessageText
|
|
@ -0,0 +1,30 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import OverlayTechnologies from 'shared/components/OverlayTechnologies'
|
||||
|
||||
const TICKscriptOverlay = ({tickscript, onClose}) =>
|
||||
<OverlayTechnologies>
|
||||
<div className="tick-script-overlay">
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Generated TICKscript</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="write-data-form--body">
|
||||
<pre className="tick-script-overlay--sample">
|
||||
{tickscript}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTechnologies>
|
||||
|
||||
const {string, func} = PropTypes
|
||||
|
||||
TICKscriptOverlay.propTypes = {
|
||||
tickscript: string,
|
||||
onClose: func.isRequired,
|
||||
}
|
||||
|
||||
export default TICKscriptOverlay
|
|
@ -28,7 +28,9 @@ export const ValuesSection = React.createClass({
|
|||
<Tabs initialIndex={initialIndex} onSelect={this.handleChooseTrigger}>
|
||||
<TabList isKapacitorTabs="true">
|
||||
{TABS.map(tab =>
|
||||
<Tab key={tab} isKapacitorTab={true}>{tab}</Tab>
|
||||
<Tab key={tab} isKapacitorTab={true}>
|
||||
{tab}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue