Merge remote-tracking branch 'origin/master' into fix/ie11-support

pull/10616/head
Hunter Trujillo 2017-07-27 17:08:38 -06:00
commit 5777552296
156 changed files with 7894 additions and 5719 deletions

View File

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

View File

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

21
Gopkg.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

93
influx/authorization.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

60
server/cells_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,6 +83,7 @@ export const RuleHeader = React.createClass({
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
{saveButton}
<ReactTooltip

View File

@ -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 &quot;value&quot; }}"
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

View File

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

View File

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

View File

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

View File

@ -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 &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
)
}
}
const {func, shape} = PropTypes
RuleMessageText.propTypes = {
rule: shape().isRequired,
updateMessage: func.isRequired,
}
export default RuleMessageText

View File

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

View File

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