Merge branch 'master' into feature/template-variables

# Conflicts:
#	bolt/internal/internal.pb.go
#	ui/src/CheckSources.js
#	ui/src/dashboards/actions/index.js
#	ui/src/dashboards/containers/DashboardPage.js
#	ui/src/data_explorer/components/Visualization.js
#	ui/src/shared/components/AutoRefresh.js
#	ui/src/shared/components/Dropdown.js
pull/1347/head
Hunter Trujillo 2017-04-25 17:08:55 -06:00
commit 0d1c416c98
162 changed files with 4100 additions and 4066 deletions

View File

@ -1,21 +1,34 @@
## v1.2.0 [unreleased]
### Bug Fixes
1. [#1257](https://github.com/influxdata/chronograf/issues/1257): Fix function selection in query builder
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix env var name for Google client secret
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to query config generation
### Features
### UI Improvements
## v1.2.0-beta9 [2017-04-21]
### Bug Fixes
1. [#1257](https://github.com/influxdata/chronograf/issues/1257): Fix function selection in the query builder
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix the environment variable name for Google client secret
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to the explorer's query generation process
1. [#1318](https://github.com/influxdata/chronograf/issues/1318): Fix JWT refresh for auth-durations of zero and less than five minutes
1. [#1332](https://github.com/influxdata/chronograf/pull/1332): Remove table toggle from dashboard visualization
### Features
1. [#1292](https://github.com/influxdata/chronograf/pull/1292): Introduce Template Variable Manager
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
1. [#1265](https://github.com/influxdata/chronograf/pull/1265): Refactor the router to use auth and force /login route when auth expires
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
1. [#1316](https://github.com/influxdata/chronograf/pull/1316): Add templates API scoped within a dashboard
1. [#1311](https://github.com/influxdata/chronograf/pull/1311): Display currently selected values in TVControlBar
1. [#1315](https://github.com/influxdata/chronograf/pull/1315): Send selected TV values to proxy
1. [#1302](https://github.com/influxdata/chronograf/pull/1302): Add support for multiple Kapacitors per InfluxDB source
### UI Improvements
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add default display for empty dashboard
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add a default display for empty dashboard
1. [#1258](https://github.com/influxdata/chronograf/pull/1258): Display Kapacitor alert endpoint options as radio button group
1. [#1321](https://github.com/influxdata/chronograf/pull/1321): Add yellow color to UI, Query Editor warnings are now appropriately colored
## v1.2.0-beta8 [2017-04-07]

View File

@ -59,8 +59,8 @@ Currently, Chronograf offers dashboard templates for the following Telegraf inpu
Chronograf's graphing tool that allows you to dig in and create personalized visualizations of your data.
* Generate [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the query builder
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the raw query editor
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the query editor
* Use Chronograf's query templates to easily explore your data
* Create visualizations and view query results in tabular format
### Dashboards
@ -91,6 +91,7 @@ A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and
* [VictorOps](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#victorops)
* View all active alerts at a glance on the alerting dashboard
* Enable and disable existing alert rules with the check of a box
* Configure multiple Kapacitor instances per InfluxDB source
### User and Query Management
@ -110,7 +111,7 @@ Change the default root path of the Chronograf server with the `--basepath` opti
## Versions
Chronograf v1.2.0-beta8 is a beta release.
Chronograf v1.2.0-beta9 is a beta release.
We will be iterating quickly based on user feedback and recommend using the [nightly builds](https://www.influxdata.com/downloads/) for the time being.
Spotted a bug or have a feature request?

View File

@ -54,6 +54,7 @@ func MarshalServer(s chronograf.Server) ([]byte, error) {
Username: s.Username,
Password: s.Password,
URL: s.URL,
Active: s.Active,
})
}
@ -70,6 +71,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
s.Username = pb.Username
s.Password = pb.Password
s.URL = pb.URL
s.Active = pb.Active
return nil
}

View File

@ -12,9 +12,6 @@ It has these top-level messages:
Source
Dashboard
DashboardCell
Template
TemplateValue
TemplateQuery
Server
Layout
Cell
@ -59,10 +56,9 @@ func (*Source) ProtoMessage() {}
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
type Dashboard struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
Templates []*Template `protobuf:"bytes,4,rep,name=templates" json:"templates,omitempty"`
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
}
func (m *Dashboard) Reset() { *m = Dashboard{} }
@ -77,13 +73,6 @@ func (m *Dashboard) GetCells() []*DashboardCell {
return nil
}
func (m *Dashboard) GetTemplates() []*Template {
if m != nil {
return m.Templates
}
return nil
}
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"`
@ -107,59 +96,6 @@ func (m *DashboardCell) GetQueries() []*Query {
return nil
}
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"`
Values []*TemplateValue `protobuf:"bytes,3,rep,name=values" json:"values,omitempty"`
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
Label string `protobuf:"bytes,5,opt,name=label,proto3" json:"label,omitempty"`
Query *TemplateQuery `protobuf:"bytes,6,opt,name=query" json:"query,omitempty"`
}
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 (m *Template) GetValues() []*TemplateValue {
if m != nil {
return m.Values
}
return nil
}
func (m *Template) GetQuery() *TemplateQuery {
if m != nil {
return m.Query
}
return nil
}
type TemplateValue struct {
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"`
}
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} }
type TemplateQuery struct {
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
Db string `protobuf:"bytes,2,opt,name=db,proto3" json:"db,omitempty"`
Rp string `protobuf:"bytes,3,opt,name=rp,proto3" json:"rp,omitempty"`
Measurement string `protobuf:"bytes,4,opt,name=measurement,proto3" json:"measurement,omitempty"`
TagKey string `protobuf:"bytes,5,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"`
FieldKey string `protobuf:"bytes,6,opt,name=field_key,json=fieldKey,proto3" json:"field_key,omitempty"`
}
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} }
type Server struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
@ -167,12 +103,13 @@ type Server struct {
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
}
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{3} }
type Layout struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
@ -185,7 +122,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{4} }
func (m *Layout) GetCells() []*Cell {
if m != nil {
@ -210,7 +147,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{5} }
func (m *Cell) GetQueries() []*Query {
if m != nil {
@ -232,7 +169,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{6} }
func (m *Query) GetRange() *Range {
if m != nil {
@ -249,7 +186,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{7} }
type AlertRule struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
@ -261,7 +198,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{8} }
type User struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
@ -271,15 +208,12 @@ 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{9} }
func init() {
proto.RegisterType((*Source)(nil), "internal.Source")
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
proto.RegisterType((*Template)(nil), "internal.Template")
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
proto.RegisterType((*TemplateQuery)(nil), "internal.TemplateQuery")
proto.RegisterType((*Server)(nil), "internal.Server")
proto.RegisterType((*Layout)(nil), "internal.Layout")
proto.RegisterType((*Cell)(nil), "internal.Cell")
@ -292,58 +226,47 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 848 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xdd, 0x2d, 0x68, 0xb4, 0x62, 0x0d, 0xdc, 0x44, 0x16, 0x48,
0x01, 0x89, 0x82, 0xd8, 0x27, 0x68, 0x6b, 0x09, 0x85, 0x76, 0x97, 0x32, 0x69, 0xcb, 0x15, 0x5a,
0x4d, 0x92, 0x93, 0xd6, 0xda, 0x49, 0x6c, 0xc6, 0x76, 0xb3, 0x7e, 0x05, 0xc4, 0x05, 0x4f, 0x80,
0xc4, 0x2d, 0x97, 0xbc, 0x00, 0x0f, 0xc1, 0x0b, 0xa1, 0x33, 0x33, 0xfe, 0x89, 0xb6, 0xa0, 0xbd,
0xe2, 0x6e, 0xbe, 0x73, 0x26, 0xdf, 0x9c, 0x9f, 0xef, 0x73, 0xe0, 0x28, 0xdb, 0x55, 0xa8, 0x77,
0x52, 0x1d, 0x17, 0x3a, 0xaf, 0x72, 0x1e, 0xb6, 0x38, 0xf9, 0x79, 0x04, 0xe3, 0x45, 0x5e, 0xeb,
0x15, 0xf2, 0x23, 0x18, 0xcd, 0xd3, 0x98, 0x4d, 0xd9, 0xcc, 0x13, 0xa3, 0x79, 0xca, 0x39, 0xf8,
0x2f, 0xe5, 0x16, 0xe3, 0xd1, 0x94, 0xcd, 0x22, 0x61, 0xce, 0x14, 0xbb, 0x6a, 0x0a, 0x8c, 0x3d,
0x1b, 0xa3, 0x33, 0xff, 0x08, 0xc2, 0xeb, 0x92, 0xd8, 0xb6, 0x18, 0xfb, 0x26, 0xde, 0x61, 0xca,
0x5d, 0xca, 0xb2, 0xdc, 0xe7, 0x7a, 0x1d, 0x07, 0x36, 0xd7, 0x62, 0xfe, 0x3e, 0x78, 0xd7, 0xe2,
0x22, 0x1e, 0x9b, 0x30, 0x1d, 0x79, 0x0c, 0x93, 0x14, 0x37, 0xb2, 0x56, 0x55, 0x3c, 0x99, 0xb2,
0x59, 0x28, 0x5a, 0x48, 0x3c, 0x57, 0xa8, 0xf0, 0x56, 0xcb, 0x4d, 0x1c, 0x5a, 0x9e, 0x16, 0xf3,
0x63, 0xe0, 0xf3, 0x5d, 0x89, 0xab, 0x5a, 0xe3, 0xe2, 0x75, 0x56, 0xdc, 0xa0, 0xce, 0x36, 0x4d,
0x1c, 0x19, 0x82, 0x07, 0x32, 0xf4, 0xca, 0x0b, 0xac, 0x24, 0xbd, 0x0d, 0x86, 0xaa, 0x85, 0xc9,
0xaf, 0x0c, 0xa2, 0x54, 0x96, 0x77, 0xcb, 0x5c, 0xea, 0xf5, 0x3b, 0xcd, 0xe3, 0x0b, 0x08, 0x56,
0xa8, 0x54, 0x19, 0x7b, 0x53, 0x6f, 0xf6, 0xe8, 0xeb, 0x67, 0xc7, 0xdd, 0xa0, 0x3b, 0x9e, 0x33,
0x54, 0x4a, 0xd8, 0x5b, 0xfc, 0x2b, 0x88, 0x2a, 0xdc, 0x16, 0x4a, 0x56, 0x58, 0xc6, 0xbe, 0xf9,
0x09, 0xef, 0x7f, 0x72, 0xe5, 0x52, 0xa2, 0xbf, 0x94, 0xfc, 0xc1, 0xe0, 0xc9, 0x01, 0x15, 0x7f,
0x0c, 0xec, 0x8d, 0xa9, 0x2a, 0x10, 0xec, 0x0d, 0xa1, 0xc6, 0x54, 0x14, 0x08, 0xd6, 0x10, 0xda,
0x9b, 0xdd, 0x04, 0x82, 0xed, 0x09, 0xdd, 0x99, 0x8d, 0x04, 0x82, 0xdd, 0xf1, 0xcf, 0x60, 0xf2,
0x53, 0x8d, 0x3a, 0xc3, 0x32, 0x0e, 0xcc, 0xcb, 0xef, 0xf5, 0x2f, 0x7f, 0x5f, 0xa3, 0x6e, 0x44,
0x9b, 0xa7, 0x4e, 0xcd, 0x36, 0xed, 0x6a, 0xcc, 0x99, 0x62, 0x15, 0x6d, 0x7e, 0x62, 0x63, 0x74,
0x76, 0x13, 0xb2, 0xfb, 0x18, 0xcd, 0xd3, 0xe4, 0x2f, 0x46, 0x6b, 0xb2, 0xa5, 0x0f, 0xc6, 0x67,
0x92, 0xfc, 0x43, 0x08, 0xa9, 0xad, 0x57, 0xf7, 0x52, 0xbb, 0x11, 0x4e, 0x08, 0xdf, 0x48, 0xcd,
0xbf, 0x84, 0xf1, 0xbd, 0x54, 0x35, 0x3e, 0x30, 0xc6, 0x96, 0xee, 0x86, 0xf2, 0xc2, 0x5d, 0xeb,
0x8a, 0xf1, 0x07, 0xc5, 0x3c, 0x85, 0x40, 0xc9, 0x25, 0x2a, 0xa7, 0x33, 0x0b, 0x68, 0x41, 0xd4,
0x55, 0x63, 0x7a, 0x79, 0x90, 0xd9, 0xf6, 0x6e, 0x6f, 0x25, 0xd7, 0xf0, 0xe4, 0xe0, 0xc5, 0xee,
0x25, 0x76, 0xf8, 0x92, 0xa9, 0xc3, 0xb5, 0x61, 0x01, 0x49, 0xb4, 0x44, 0x85, 0xab, 0x0a, 0xd7,
0x66, 0x05, 0xa1, 0xe8, 0x70, 0xf2, 0x3b, 0xeb, 0x79, 0xcd, 0x7b, 0x24, 0xc2, 0x55, 0xbe, 0xdd,
0xca, 0xdd, 0xda, 0x51, 0xb7, 0x90, 0xe6, 0xb6, 0x5e, 0x3a, 0xea, 0xd1, 0x7a, 0x49, 0x58, 0x17,
0xce, 0x70, 0x23, 0x5d, 0xf0, 0x29, 0x3c, 0xda, 0xa2, 0x2c, 0x6b, 0x8d, 0x5b, 0xdc, 0x55, 0x6e,
0x04, 0xc3, 0x10, 0x7f, 0x06, 0x93, 0x4a, 0xde, 0xbe, 0x7a, 0x8d, 0x8d, 0x9b, 0xc5, 0xb8, 0x92,
0xb7, 0xe7, 0xd8, 0xf0, 0x8f, 0x21, 0xda, 0x64, 0xa8, 0xd6, 0x26, 0x65, 0x97, 0x1b, 0x9a, 0xc0,
0x39, 0x36, 0xc9, 0x2f, 0x0c, 0xc6, 0x0b, 0xd4, 0xf7, 0xa8, 0xdf, 0x49, 0xf9, 0x43, 0xd7, 0x7b,
0xff, 0xe1, 0x7a, 0xff, 0x61, 0xd7, 0x07, 0xbd, 0xeb, 0x9f, 0x42, 0xb0, 0xd0, 0xab, 0x79, 0x6a,
0x2a, 0xf2, 0x84, 0x05, 0xc9, 0x6f, 0x0c, 0xc6, 0x17, 0xb2, 0xc9, 0xeb, 0xea, 0x2d, 0x25, 0x4d,
0xe1, 0xd1, 0x49, 0x51, 0xa8, 0x6c, 0x25, 0xab, 0x2c, 0xdf, 0xb9, 0xaa, 0x86, 0x21, 0xba, 0xf1,
0x62, 0x30, 0x23, 0x5b, 0xdf, 0x30, 0xc4, 0x3f, 0x81, 0xe0, 0xcc, 0x18, 0xd7, 0xba, 0xf0, 0xa8,
0xd7, 0x85, 0xf5, 0xab, 0x49, 0x52, 0x23, 0x27, 0x75, 0x95, 0x6f, 0x54, 0xbe, 0x37, 0x15, 0x87,
0xa2, 0xc3, 0xc9, 0xdf, 0x0c, 0xfc, 0xff, 0xcb, 0x90, 0x8f, 0x81, 0x65, 0x6e, 0x61, 0x2c, 0xeb,
0xec, 0x39, 0x19, 0xd8, 0x33, 0x86, 0x49, 0xa3, 0xe5, 0xee, 0x16, 0xcb, 0x38, 0x9c, 0x7a, 0x33,
0x4f, 0xb4, 0xd0, 0x64, 0x8c, 0x17, 0xca, 0x38, 0x9a, 0x7a, 0xa4, 0x34, 0x07, 0x3b, 0x6d, 0x43,
0xaf, 0xed, 0xe4, 0x4f, 0x06, 0x41, 0xa7, 0xd0, 0xb3, 0x43, 0x85, 0x9e, 0xf5, 0x0a, 0x4d, 0x4f,
0x5b, 0x85, 0xa6, 0xa7, 0x84, 0xc5, 0x65, 0xab, 0x50, 0x71, 0x49, 0x53, 0xfb, 0x46, 0xe7, 0x75,
0x71, 0xda, 0xd8, 0xf1, 0x46, 0xa2, 0xc3, 0xfc, 0x03, 0x18, 0xff, 0x70, 0x87, 0xda, 0xf5, 0x1c,
0x09, 0x87, 0x48, 0x04, 0x17, 0xc6, 0xbd, 0xb6, 0x4b, 0x0b, 0xf8, 0xa7, 0x10, 0x08, 0xea, 0xc2,
0xb4, 0x7a, 0x30, 0x20, 0x13, 0x16, 0x36, 0x9b, 0x3c, 0x77, 0xd7, 0x88, 0xe5, 0xba, 0x28, 0x50,
0x3b, 0xed, 0x5a, 0x60, 0xb8, 0xf3, 0x3d, 0xda, 0xcf, 0x8e, 0x27, 0x2c, 0x48, 0x7e, 0x84, 0xe8,
0x44, 0xa1, 0xae, 0x44, 0xad, 0xde, 0xfe, 0x58, 0x71, 0xf0, 0xbf, 0x5d, 0x7c, 0xf7, 0xb2, 0x55,
0x3c, 0x9d, 0x7b, 0x9d, 0x7a, 0x03, 0x9d, 0x52, 0x43, 0xe7, 0xb2, 0x90, 0xf3, 0xd4, 0x2c, 0xd6,
0x13, 0x0e, 0x25, 0x9f, 0x83, 0x4f, 0x7e, 0x18, 0x30, 0xfb, 0xff, 0xe6, 0xa5, 0xe5, 0xd8, 0xfc,
0x2b, 0x3f, 0xff, 0x27, 0x00, 0x00, 0xff, 0xff, 0xdd, 0x04, 0x69, 0xb6, 0xa7, 0x07, 0x00, 0x00,
// 670 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xcd, 0x6e, 0xd3, 0x4a,
0x14, 0xd6, 0xc4, 0x76, 0x7e, 0x4e, 0x7b, 0x7b, 0xaf, 0x46, 0x57, 0x30, 0x62, 0x15, 0x59, 0x20,
0x05, 0x24, 0xba, 0xa0, 0x4f, 0x90, 0xd6, 0x12, 0x0a, 0xb4, 0xa5, 0x4c, 0x5a, 0x58, 0x81, 0x34,
0x4d, 0x4f, 0x1a, 0x0b, 0xc7, 0x36, 0x63, 0xbb, 0xa9, 0x5f, 0x81, 0x87, 0x60, 0xc5, 0x8a, 0x25,
0xaf, 0xc2, 0x0b, 0xa1, 0x33, 0x33, 0x76, 0x52, 0x28, 0xa8, 0x2b, 0x76, 0xe7, 0x3b, 0xc7, 0x39,
0x3f, 0xdf, 0xf7, 0x4d, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
0xef, 0x37, 0x38, 0xfc, 0xd4, 0x81, 0xee, 0x34, 0xab, 0xf4, 0x0c, 0xf9, 0x0e, 0x74, 0x26, 0x91,
0x60, 0x43, 0x36, 0xf2, 0x64, 0x67, 0x12, 0x71, 0x0e, 0xfe, 0xb1, 0x5a, 0xa2, 0xe8, 0x0c, 0xd9,
0x68, 0x20, 0x4d, 0x4c, 0xb9, 0xd3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0xf4, 0xcf,
0x0a, 0xea, 0xb6, 0x44, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xa2, 0x8a, 0x62, 0x95, 0xe9, 0x0b,
0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xc9, 0x43, 0xd1, 0x35, 0x69, 0x0a, 0xb9, 0x80,
0x5e, 0x84, 0x73, 0x55, 0x25, 0xa5, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x1b, 0x48, 0x7d, 0x4e, 0x31,
0xc1, 0x4b, 0xad, 0xe6, 0xa2, 0x6f, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa4, 0x05, 0xce, 0x2a,
0x8d, 0xd3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0x3c, 0xaf, 0xc5, 0xc0, 0x34, 0xb8, 0xa5, 0x42, 0x53,
0x8e, 0xb0, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x10, 0xa9, 0x62, 0x71, 0x9e,
0x29, 0x7d, 0x71, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc3, 0x24, 0x29, 0x84, 0x37, 0xf4, 0x46, 0x5b,
0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x30, 0x49, 0xa4, 0xfd, 0x2a, 0xfc, 0xca, 0xe0,
0x9f, 0x1b, 0x05, 0xbe, 0x0d, 0xec, 0xda, 0xcc, 0x08, 0x24, 0xbb, 0x26, 0x54, 0x9b, 0xfe, 0x81,
0x64, 0x35, 0xa1, 0x95, 0x21, 0x3a, 0x90, 0x6c, 0x45, 0x68, 0x61, 0xe8, 0x0d, 0x24, 0x5b, 0xf0,
0xc7, 0xd0, 0xfb, 0x58, 0xa1, 0x8e, 0xb1, 0x10, 0x81, 0x19, 0xfd, 0xef, 0x7a, 0xf4, 0xeb, 0x0a,
0x75, 0x2d, 0x9b, 0x3a, 0xed, 0x6d, 0xa4, 0xb1, 0x3c, 0x9b, 0x98, 0x72, 0x25, 0xc9, 0xd8, 0xb3,
0x39, 0x8a, 0xdd, 0xbd, 0x96, 0xdc, 0xce, 0x24, 0x0a, 0xbf, 0x30, 0xe8, 0x4e, 0x51, 0x5f, 0xa1,
0xbe, 0x13, 0x15, 0x9b, 0x2e, 0xf0, 0xfe, 0xe0, 0x02, 0xff, 0x76, 0x17, 0x04, 0x6b, 0x17, 0xfc,
0x0f, 0xc1, 0x54, 0xcf, 0x26, 0x91, 0xd9, 0xd8, 0x93, 0x16, 0xf0, 0x7b, 0xd0, 0x1d, 0xcf, 0xca,
0xf8, 0x0a, 0x9d, 0x35, 0x1c, 0x0a, 0x3f, 0x33, 0xe8, 0x1e, 0xaa, 0x3a, 0xab, 0xca, 0x8d, 0x35,
0xcd, 0x05, 0x7c, 0x08, 0x5b, 0xe3, 0x3c, 0x4f, 0xe2, 0x99, 0x2a, 0xe3, 0x2c, 0x75, 0xdb, 0x6e,
0xa6, 0xe8, 0x8b, 0x23, 0x54, 0x45, 0xa5, 0x71, 0x89, 0x69, 0xe9, 0xf6, 0xde, 0x4c, 0xf1, 0x87,
0x10, 0x1c, 0x18, 0x85, 0x7d, 0x43, 0xf3, 0xce, 0x9a, 0x66, 0x2b, 0xac, 0x29, 0xd2, 0x81, 0xe3,
0xaa, 0xcc, 0xe6, 0x49, 0xb6, 0x32, 0x97, 0xf4, 0x65, 0x8b, 0xc3, 0xef, 0x0c, 0xfc, 0xbf, 0xa5,
0xf5, 0x36, 0xb0, 0xd8, 0x09, 0xcd, 0xe2, 0x56, 0xf9, 0xde, 0x86, 0xf2, 0x02, 0x7a, 0xb5, 0x56,
0xe9, 0x25, 0x16, 0xa2, 0x3f, 0xf4, 0x46, 0x9e, 0x6c, 0xa0, 0xa9, 0x24, 0xea, 0x1c, 0x93, 0x42,
0x0c, 0x86, 0x1e, 0x3d, 0x0b, 0x07, 0x5b, 0xb7, 0xc0, 0xda, 0x2d, 0xe1, 0x37, 0x06, 0x81, 0x19,
0x4e, 0xbf, 0x3b, 0xc8, 0x96, 0x4b, 0x95, 0x5e, 0x38, 0xea, 0x1b, 0x48, 0x7a, 0x44, 0xfb, 0x8e,
0xf6, 0x4e, 0xb4, 0x4f, 0x58, 0x9e, 0x38, 0x92, 0x3b, 0xf2, 0x84, 0x58, 0x7b, 0xae, 0xb3, 0x2a,
0xdf, 0xaf, 0x2d, 0xbd, 0x03, 0xd9, 0x62, 0x92, 0xfb, 0xed, 0x02, 0xb5, 0xbb, 0x79, 0x20, 0x1d,
0x22, 0x73, 0x1c, 0xd2, 0x56, 0xee, 0x4a, 0x0b, 0xf8, 0x23, 0x08, 0x24, 0x5d, 0x61, 0x4e, 0xbd,
0x41, 0x90, 0x49, 0x4b, 0x5b, 0x0d, 0xf7, 0xdc, 0x67, 0xd4, 0xe5, 0x2c, 0xcf, 0x51, 0x3b, 0x4f,
0x5b, 0x60, 0x7a, 0x67, 0x2b, 0xd4, 0x66, 0x65, 0x4f, 0x5a, 0x10, 0xbe, 0x83, 0xc1, 0x38, 0x41,
0x5d, 0xca, 0x2a, 0xc1, 0x5f, 0x2c, 0xc6, 0xc1, 0x7f, 0x31, 0x7d, 0x75, 0xdc, 0xbc, 0x04, 0x8a,
0xd7, 0xfe, 0xf5, 0x7e, 0xf2, 0xef, 0x4b, 0x95, 0xab, 0x49, 0x64, 0x84, 0xf5, 0xa4, 0x43, 0xe1,
0x13, 0xf0, 0xe9, 0x9d, 0x6c, 0x74, 0xf6, 0x7f, 0xf7, 0xc6, 0xce, 0xbb, 0xe6, 0xdf, 0x7b, 0xef,
0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0xbe, 0xb0, 0xc3, 0xcf, 0x05, 0x00, 0x00,
}

View File

@ -63,6 +63,7 @@ message 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
bool Active = 7; // is this the currently active server for the source
}
message Layout {

View File

@ -24,14 +24,9 @@ type ServersStore struct {
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
var srcs []chronograf.Server
if err := s.client.db.View(func(tx *bolt.Tx) error {
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
var src chronograf.Server
if err := internal.UnmarshalServer(v, &src); err != nil {
return err
}
srcs = append(srcs, src)
return nil
}); err != nil {
var err error
srcs, err = s.all(ctx, tx)
if err != nil {
return err
}
return nil
@ -53,6 +48,10 @@ func (s *ServersStore) Add(ctx context.Context, src chronograf.Server) (chronogr
}
src.ID = int(seq)
// make the newly added source "active"
s.resetActiveServer(ctx, tx)
src.Active = true
if v, err := internal.MarshalServer(src); err != nil {
return err
} else if err := b.Put(itob(src.ID), v); err != nil {
@ -106,6 +105,11 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
return chronograf.ErrServerNotFound
}
// only one server can be active at a time
if src.Active {
s.resetActiveServer(ctx, tx)
}
if v, err := internal.MarshalServer(src); err != nil {
return err
} else if err := b.Put(itob(src.ID), v); err != nil {
@ -118,3 +122,39 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
return nil
}
func (s *ServersStore) all(ctx context.Context, tx *bolt.Tx) ([]chronograf.Server, error) {
var srcs []chronograf.Server
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
var src chronograf.Server
if err := internal.UnmarshalServer(v, &src); err != nil {
return err
}
srcs = append(srcs, src)
return nil
}); err != nil {
return srcs, err
}
return srcs, nil
}
// resetActiveServer unsets the Active flag on all sources
func (s *ServersStore) resetActiveServer(ctx context.Context, tx *bolt.Tx) error {
b := tx.Bucket(ServersBucket)
srcs, err := s.all(ctx, tx)
if err != nil {
return err
}
for _, other := range srcs {
if other.Active {
other.Active = false
if v, err := internal.MarshalServer(other); err != nil {
return err
} else if err := b.Put(itob(other.ID), v); err != nil {
return err
}
}
}
return nil
}

View File

@ -27,6 +27,7 @@ func TestServerStore(t *testing.T) {
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
},
chronograf.Server{
Name: "HipToBeSquare",
@ -34,6 +35,7 @@ func TestServerStore(t *testing.T) {
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
},
}
@ -72,6 +74,21 @@ func TestServerStore(t *testing.T) {
t.Fatalf("server 1 update error: got %v, expected %v", src.Name, "Enchantment Under the Sea Dance")
}
// Attempt to make two active sources
srcs[0].Active = true
srcs[1].Active = true
if err := s.Update(ctx, srcs[0]); err != nil {
t.Fatal(err)
} else if err := s.Update(ctx, srcs[1]); err != nil {
t.Fatal(err)
}
if actual, err := s.Get(ctx, srcs[0].ID); err != nil {
t.Fatal(err)
} else if actual.Active == true {
t.Fatal("Able to set two active servers when only one should be permitted")
}
// Delete an server.
if err := s.Delete(ctx, srcs[0]); err != nil {
t.Fatal(err)

View File

@ -324,6 +324,7 @@ type Server struct {
Username string // Username is the username to connect to the server
Password string // Password is in CLEARTEXT
URL string // URL are the connections to the server
Active bool // Is this the active server for the source?
}
// ServersStore stores connection information for a `Server`

View File

@ -195,8 +195,8 @@ Now that we are collecting data with Telegraf and storing data with InfluxDB, it
#### 1. Download and Install Chronograf
```
wget https://dl.influxdata.com/chronograf/releases/chronograf_1.2.0~beta8_amd64.deb
sudo dpkg -i chronograf_1.2.0~beta8_amd64.deb
wget https://dl.influxdata.com/chronograf/releases/chronograf_1.2.0~beta9_amd64.deb
sudo dpkg -i chronograf_1.2.0~beta9_amd64.deb
```
#### 2. Start Chronograf

View File

@ -26,10 +26,18 @@ type cookie struct {
// NewCookieJWT creates an Authenticator that uses cookies for auth
func NewCookieJWT(secret string, lifespan time.Duration) Authenticator {
inactivity := DefaultInactivityDuration
// Server interprets a token duration longer than the cookie lifespan as
// a token that was issued by a server with a longer auth-duration and is
// thus invalid, as a security precaution. So, inactivity must be set to
// be less than lifespan.
if lifespan > 0 && inactivity > lifespan {
inactivity = lifespan / 2 // half of the lifespan ensures tokens can be refreshed once.
}
return &cookie{
Name: DefaultCookieName,
Lifespan: lifespan,
Inactivity: DefaultInactivityDuration,
Inactivity: inactivity,
Now: DefaultNowTime,
Tokens: &JWT{
Secret: secret,
@ -44,6 +52,7 @@ func (c *cookie) Validate(ctx context.Context, r *http.Request) (Principal, erro
if err != nil {
return Principal{}, ErrAuthentication
}
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Lifespan)
}
@ -105,15 +114,22 @@ func (c *cookie) setCookie(w http.ResponseWriter, value string, exp time.Time) {
// Only set a cookie to be persistent (endure beyond the browser session)
// if auth duration is greater than zero
if c.Lifespan > 0 || exp.Before(c.Now()) {
if c.Lifespan > 0 {
cookie.Expires = exp
}
http.SetCookie(w, &cookie)
}
// Expire returns a cookie that will expire an existing cookie
func (c *cookie) Expire(w http.ResponseWriter) {
// to expire cookie set the time in the past
c.setCookie(w, "none", c.Now().Add(-1*time.Hour))
cookie := http.Cookie{
Name: DefaultCookieName,
Value: "none",
HttpOnly: true,
Path: "/",
Expires: c.Now().Add(-1 * time.Hour),
}
http.SetCookie(w, &cookie)
}

View File

@ -152,9 +152,25 @@ func TestCookieValidate(t *testing.T) {
}
func TestNewCookieJWT(t *testing.T) {
auth := NewCookieJWT("secret", time.Second)
if _, ok := auth.(*cookie); !ok {
auth := NewCookieJWT("secret", 2*time.Second)
if cookie, ok := auth.(*cookie); !ok {
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
} else if cookie.Inactivity != time.Second {
t.Errorf("NewCookieJWT() inactivity was not two seconds: %s", cookie.Inactivity)
}
auth = NewCookieJWT("secret", time.Hour)
if cookie, ok := auth.(*cookie); !ok {
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
} else if cookie.Inactivity != DefaultInactivityDuration {
t.Errorf("NewCookieJWT() inactivity was not five minutes: %s", cookie.Inactivity)
}
auth = NewCookieJWT("secret", 0)
if cookie, ok := auth.(*cookie); !ok {
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
} else if cookie.Inactivity != DefaultInactivityDuration {
t.Errorf("NewCookieJWT() inactivity was not five minutes: %s", cookie.Inactivity)
}
}

View File

@ -45,7 +45,7 @@ func (c *Claims) Valid() error {
}
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims. lifespan is the
// maximum valid lifetime of a token.
// maximum valid lifetime of a token. If the lifespan is 0 then the auth lifespan duration is not checked.
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error) {
gojwt.TimeFunc = j.Now
@ -86,8 +86,9 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
// If the duration of the claim is longer than the auth lifespan then this is
// an invalid claim because server assumes that lifespan is the maximum possible
// duration
if exp.Sub(iat) > lifespan {
// duration. However, a lifespan of zero means that the duration comparison
// against the auth duration is not needed.
if lifespan > 0 && exp.Sub(iat) > lifespan {
return Principal{}, fmt.Errorf("claims duration is different from auth lifespan")
}

View File

@ -79,14 +79,14 @@ func TestAuthenticate(t *testing.T) {
{
Desc: "Test jwt duration matches auth duration",
Secret: "secret",
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI",
Duration: 500 * time.Hour,
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzMDAsImlhdCI6LTQ0Njc3NDQwMCwiaXNzIjoiaGlsbHZhbGxleSIsIm5iZiI6LTQ0Njc3NDQwMCwic3ViIjoibWFydHlAcGluaGVhZC5uZXQifQ.njEjstpuIDnghSR7VyPPB9QlvJ6Q5JpR3ZEZ_8vGYfA",
Duration: time.Second,
Principal: oauth2.Principal{
Subject: "/chronograf/v1/users/1",
Subject: "marty@pinhead.net",
ExpiresAt: history,
IssuedAt: history,
IssuedAt: history.Add(100 * time.Second),
},
Err: errors.New("claims duration is different from auth duration"),
Err: errors.New("claims duration is different from auth lifespan"),
},
}
for _, test := range tests {
@ -97,6 +97,9 @@ func TestAuthenticate(t *testing.T) {
},
}
principal, err := j.ValidPrincipal(context.Background(), test.Token, test.Duration)
if test.Err != nil && err == nil {
t.Fatalf("Expected err %s", test.Err.Error())
}
if err != nil {
if test.Err == nil {
t.Errorf("Error in test %s authenticating with bad token: %v", test.Desc, err)

View File

@ -17,6 +17,7 @@ type postKapacitorRequest struct {
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
Active bool `json:"active"`
}
func (p *postKapacitorRequest) Valid() error {
@ -47,6 +48,7 @@ type kapacitor struct {
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
Active bool `json:"active"`
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
}
@ -81,6 +83,7 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
Username: req.Username,
Password: req.Password,
URL: *req.URL,
Active: req.Active,
}
if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
@ -102,6 +105,7 @@ func newKapacitor(srv chronograf.Server) kapacitor {
Username: srv.Username,
Password: srv.Password,
URL: srv.URL,
Active: srv.Active,
Links: kapaLinks{
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
@ -217,6 +221,7 @@ type patchKapacitorRequest struct {
URL *string `json:"url,omitempty"` // URL for the kapacitor
Username *string `json:"username,omitempty"` // Username for kapacitor auth
Password *string `json:"password,omitempty"`
Active *bool `json:"active"`
}
func (p *patchKapacitorRequest) Valid() error {
@ -276,6 +281,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
if req.Username != nil {
srv.Username = *req.Username
}
if req.Active != nil {
srv.Active = *req.Active
}
if err := h.ServersStore.Update(ctx, srv); err != nil {
msg := fmt.Sprintf("Error updating kapacitor ID %d", id)

View File

@ -1154,7 +1154,7 @@
"required": true
}
],
"summary": "Configured kapacitors",
"summary": "Retrieve list of configured kapacitors",
"responses": {
"200": {
"description": "An array of kapacitors",
@ -1239,7 +1239,7 @@
}
],
"summary": "Configured kapacitors",
"description": "These kapacitors are used for monitoring and alerting.",
"description": "Retrieve information on a single kapacitor instance",
"responses": {
"200": {
"description": "Kapacitor connection information",
@ -1334,7 +1334,8 @@
"required": true
}
],
"summary": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
"summary": "Remove Kapacitor backend",
"description": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
"responses": {
"204": {
"description": "kapacitor has been removed."
@ -1683,7 +1684,7 @@
"kapacitors",
"proxy"
],
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
"parameters": [
{
"name": "id",
@ -2388,6 +2389,7 @@
"id": "4",
"name": "kapa",
"url": "http://localhost:9092",
"active": false,
"links": {
"proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy",
"self": "/chronograf/v1/sources/4/kapacitors/4",
@ -2417,6 +2419,10 @@
"format": "url",
"description": "URL for the kapacitor backend (e.g. http://localhost:9092)"
},
"active": {
"type": "boolean",
"description": "Indicates whether the kapacitor is the current kapacitor being used for a source"
},
"links": {
"type": "object",
"properties": {

View File

@ -40,7 +40,7 @@
},
},
rules: {
'quotes': [0, "double"],
'quotes': [1, 'single'],
'func-style': 0,
'func-names': 0,
'arrow-parens': 0,

View File

@ -106,10 +106,12 @@
"react-grid-layout": "^0.13.9",
"react-onclickoutside": "^5.2.0",
"react-redux": "^4.4.0",
"react-router": "^2.4.1",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",
"react-sparklines": "^1.4.2",
"react-tooltip": "^3.2.1",
"redux": "^3.3.1",
"redux-auth-wrapper": "^1.0.0",
"redux-thunk": "^1.0.3",
"rome": "^2.1.22",
"updeep": "^0.13.0"

View File

@ -0,0 +1,68 @@
import {default as authReducer, initialState} from 'shared/reducers/auth'
import {
authExpired,
authRequested,
authReceived,
meRequested,
meReceived,
} from 'shared/actions/auth'
const defaultAuth = {
links: [
{
name: 'github',
label: 'Github',
login: '/oauth/github/login',
logout: '/oauth/github/logout',
callback: '/oauth/github/callback',
},
],
}
const defaultMe = {
name: 'wishful_modal@overlay.technology',
password: '',
links: {
self: '/chronograf/v1/users/wishful_modal@overlay.technology',
},
}
describe('Shared.Reducers.authReducer', () => {
it('should handle AUTH_EXPIRED', () => {
const reducedState = authReducer(initialState, authExpired(defaultAuth))
expect(reducedState.links[0]).to.deep.equal(defaultAuth.links[0])
expect(reducedState.me).to.equal(null)
expect(reducedState.isMeLoading).to.equal(false)
expect(reducedState.isAuthLoading).to.equal(false)
})
it('should handle AUTH_REQUESTED', () => {
const reducedState = authReducer(initialState, authRequested())
expect(reducedState.isAuthLoading).to.equal(true)
})
it('should handle AUTH_RECEIVED', () => {
const loadingState = Object.assign({}, initialState, {isAuthLoading: true})
const reducedState = authReducer(loadingState, authReceived(defaultAuth))
expect(reducedState.links[0]).to.deep.equal(defaultAuth.links[0])
expect(reducedState.isAuthLoading).to.equal(false)
})
it('should handle ME_REQUESTED', () => {
const reducedState = authReducer(initialState, meRequested())
expect(reducedState.isMeLoading).to.equal(true)
})
it('should handle ME_RECEIVED', () => {
const loadingState = Object.assign({}, initialState, {isMeLoading: true})
const reducedState = authReducer(loadingState, meReceived(defaultMe))
expect(reducedState.me).to.deep.equal(defaultMe)
expect(reducedState.isAuthLoading).to.equal(false)
})
})

View File

@ -0,0 +1,61 @@
import {default as errorsReducer, initialState} from 'shared/reducers/errors'
import {errorThrown} from 'shared/actions/errors'
import {HTTP_FORBIDDEN} from 'shared/constants'
const errorForbidden = {
"data":"",
"status":403,
"statusText":"Forbidden",
"headers":{
"date":"Mon, 17 Apr 2017 18:35:34 GMT",
"content-length":"0",
"x-chronograf-version":"1.2.0-beta8-71-gd875ea4a",
"content-type":"text/plain; charset=utf-8"
},
"config":{
"transformRequest":{
},
"transformResponse":{
},
"headers":{
"Accept":"application/json, text/plain, *\/*",
"Content-Type":"application/json;charset=utf-8"
},
"timeout":0,
"xsrfCookieName":"XSRF-TOKEN",
"xsrfHeaderName":"X-XSRF-TOKEN",
"maxContentLength":-1,
"method":"GET",
"url":"/chronograf/v1/me",
"data":"{}",
"params":{
}
},
"request":{
},
"auth":{
"links":[
{
"name":"github",
"label":"Github",
"login":"/oauth/github/login",
"logout":"/oauth/github/logout",
"callback":"/oauth/github/callback"
}
]
}
}
describe('Shared.Reducers.errorsReducer', () => {
it('should handle ERROR_THROWN', () => {
const reducedState = errorsReducer(initialState, errorThrown(errorForbidden))
expect(reducedState.error.status).to.equal(HTTP_FORBIDDEN)
})
})

View File

@ -1,27 +1,20 @@
import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
import SideNavContainer from 'src/side_nav'
import {bindActionCreators} from 'redux'
import SideNav from 'src/side_nav'
import Notifications from 'shared/components/Notifications'
import {
publishNotification as publishNotificationAction,
} from 'src/shared/actions/notifications'
import {publishNotification} from 'src/shared/actions/notifications'
const {
func,
node,
shape,
string,
} = PropTypes
const App = React.createClass({
propTypes: {
children: node.isRequired,
location: shape({
pathname: string,
}),
params: shape({
sourceID: string.isRequired,
}).isRequired,
notify: func.isRequired,
},
@ -32,16 +25,10 @@ const App = React.createClass({
},
render() {
const {params: {sourceID}, location} = this.props
return (
<div className="chronograf-root">
<SideNavContainer
sourceID={sourceID}
addFlashMessage={this.handleAddFlashMessage}
currentLocation={this.props.location.pathname}
/>
<Notifications location={location} />
<SideNav />
<Notifications />
{this.props.children && React.cloneElement(this.props.children, {
addFlashMessage: this.handleAddFlashMessage,
})}
@ -50,6 +37,8 @@ const App = React.createClass({
},
})
export default connect(null, {
notify: publishNotificationAction,
})(App)
const mapDispatchToProps = (dispatch) => ({
notify: bindActionCreators(publishNotification, dispatch),
})
export default connect(null, mapDispatchToProps)(App)

View File

@ -1,33 +1,33 @@
import React, {PropTypes} from 'react'
import {withRouter} from 'react-router'
import {connect} from 'react-redux'
import {getSources} from 'src/shared/apis'
import {loadSources as loadSourcesAction} from 'src/shared/actions/sources'
import {showDatabases} from 'src/shared/apis/metaQuery'
import {bindActionCreators} from 'redux'
import {getSources} from 'shared/apis'
import {showDatabases} from 'shared/apis/metaQuery'
import {loadSources as loadSourcesAction} from 'shared/actions/sources'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
// Acts as a 'router middleware'. The main `App` component is responsible for
// getting the list of data nodes, but not every page requires them to function.
// Routes that do require data nodes can be nested under this component.
const {
arrayOf,
func,
node,
shape,
string,
} = PropTypes
const {arrayOf, func, node, shape, string} = PropTypes
const CheckSources = React.createClass({
propTypes: {
sources: arrayOf(shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
kapacitors: string.isRequired,
queries: string.isRequired,
permissions: string.isRequired,
users: string.isRequired,
databases: string.isRequired,
}).isRequired,
})),
sources: arrayOf(
shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
kapacitors: string.isRequired,
queries: string.isRequired,
permissions: string.isRequired,
users: string.isRequired,
databases: string.isRequired,
}).isRequired,
})
),
addFlashMessage: func,
children: node,
params: shape({
@ -39,7 +39,8 @@ const CheckSources = React.createClass({
location: shape({
pathname: string.isRequired,
}).isRequired,
loadSourcesAction: func.isRequired,
loadSources: func.isRequired,
errorThrown: func.isRequired,
},
childContextTypes: {
@ -58,7 +59,7 @@ const CheckSources = React.createClass({
getChildContext() {
const {sources, params: {sourceID}} = this.props
return {source: sources.find((s) => s.id === sourceID)}
return {source: sources.find(s => s.id === sourceID)}
},
getInitialState() {
@ -67,56 +68,78 @@ const CheckSources = React.createClass({
}
},
componentDidMount() {
getSources().then(({data: {sources}}) => {
this.props.loadSourcesAction(sources)
async componentWillMount() {
const {loadSources, errorThrown} = this.props
try {
const {data: {sources}} = await getSources()
loadSources(sources)
this.setState({isFetching: false})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: "Unable to connect to Chronograf server"})
} catch (error) {
errorThrown(error, 'Unable to connect to Chronograf server')
this.setState({isFetching: false})
})
}
},
componentWillUpdate(nextProps, nextState) {
const {router, location, params, addFlashMessage, sources} = nextProps
async componentWillUpdate(nextProps, nextState) {
const {router, location, params, errorThrown, sources} = nextProps
const {isFetching} = nextState
const source = sources.find((s) => s.id === params.sourceID)
const defaultSource = sources.find((s) => s.default === true)
const source = sources.find(s => s.id === params.sourceID)
const defaultSource = sources.find(s => s.default === true)
if (!isFetching && !source) {
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
const restString = rest === null ? 'hosts' : rest[1]
if (defaultSource) {
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
return router.push(`/sources/${defaultSource.id}/${rest[1]}`)
return router.push(`/sources/${defaultSource.id}/${restString}`)
} else if (sources[0]) {
return router.push(`/sources/${sources[0].id}/${restString}`)
}
return router.push(`/sources/new?redirectPath=${location.pathname}`)
}
if (!isFetching && !location.pathname.includes("/manage-sources")) {
if (!isFetching && !location.pathname.includes('/manage-sources')) {
// Do simple query to proxy to see if the source is up.
showDatabases(source.links.proxy).catch(() => {
addFlashMessage({type: 'error', text: `Unable to connect to source`})
})
try {
await showDatabases(source.links.proxy)
} catch (error) {
errorThrown(error, 'Unable to connect to source')
}
}
},
render() {
const {params, sources} = this.props
const {isFetching} = this.state
const source = sources.find((s) => s.id === params.sourceID)
const source = sources.find(s => s.id === params.sourceID)
if (isFetching || !source) {
return <div className="page-spinner" />
}
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, {
source,
}))
return (
this.props.children &&
React.cloneElement(
this.props.children,
Object.assign({}, this.props, {
source,
})
)
)
},
})
function mapStateToProps(state) {
return {
sources: state.sources,
}
}
const mapStateToProps = ({sources}) => ({
sources,
})
export default connect(mapStateToProps, {loadSourcesAction})(withRouter(CheckSources))
const mapDispatchToProps = dispatch => ({
loadSources: bindActionCreators(loadSourcesAction, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(
withRouter(CheckSources)
)

View File

@ -20,8 +20,8 @@ import {
killQuery as killQueryProxy,
} from 'shared/apis/metaQuery'
import {publishNotification} from 'shared/actions/notifications'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {errorThrown} from 'shared/actions/errors'
import {REVERT_STATE_DELAY} from 'shared/constants'
@ -221,23 +221,39 @@ export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
// async actions
export const loadUsersAsync = (url) => async (dispatch) => {
const {data} = await getUsersAJAX(url)
dispatch(loadUsers(data))
try {
const {data} = await getUsersAJAX(url)
dispatch(loadUsers(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadRolesAsync = (url) => async (dispatch) => {
const {data} = await getRolesAJAX(url)
dispatch(loadRoles(data))
try {
const {data} = await getRolesAJAX(url)
dispatch(loadRoles(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadPermissionsAsync = (url) => async (dispatch) => {
const {data} = await getPermissionsAJAX(url)
dispatch(loadPermissions(data))
try {
const {data} = await getPermissionsAJAX(url)
dispatch(loadPermissions(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadDBsAndRPsAsync = (url) => async (dispatch) => {
const {data: {databases}} = await getDbsAndRpsAJAX(url)
dispatch(loadDatabases(databases))
try {
const {data: {databases}} = await getDbsAndRpsAJAX(url)
dispatch(loadDatabases(databases))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const createUserAsync = (url, user) => async (dispatch) => {
@ -246,8 +262,8 @@ export const createUserAsync = (url, user) => async (dispatch) => {
dispatch(publishAutoDismissingNotification('success', 'User created successfully'))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(errorThrown(error, `Failed to create user: ${error.data.message}`))
// undo optimistic update
dispatch(publishNotification('error', `Failed to create user: ${error.data.message}`))
setTimeout(() => dispatch(deleteUser(user)), REVERT_STATE_DELAY)
}
}
@ -258,8 +274,8 @@ export const createRoleAsync = (url, role) => async (dispatch) => {
dispatch(publishAutoDismissingNotification('success', 'Role created successfully'))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(errorThrown(error, `Failed to create role: ${error.data.message}`))
// undo optimistic update
dispatch(publishNotification('error', `Failed to create role: ${error.data.message}`))
setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY)
}
}
@ -270,8 +286,8 @@ export const createDatabaseAsync = (url, database) => async (dispatch) => {
dispatch(syncDatabase(database, data))
dispatch(publishAutoDismissingNotification('success', 'Database created successfully'))
} catch (error) {
// undo optimistic update
dispatch(publishNotification('error', `Failed to create database: ${error.data.message}`))
dispatch(errorThrown(error))
// undo optimistic upda, `Failed to create database: ${error.data.message}`te
setTimeout(() => dispatch(removeDatabase(database)), REVERT_STATE_DELAY)
}
}
@ -282,8 +298,8 @@ export const createRetentionPolicyAsync = (database, retentionPolicy) => async (
dispatch(publishAutoDismissingNotification('success', 'Retention policy created successfully'))
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
} catch (error) {
// undo optimistic update
dispatch(publishNotification('error', `Failed to create retention policy: ${error.data.message}`))
dispatch(errorThrown(error))
// undo optimistic upda, `Failed to create retention policy: ${error.data.message}`te
setTimeout(() => dispatch(removeRetentionPolicy(database, retentionPolicy)), REVERT_STATE_DELAY)
}
}
@ -295,17 +311,21 @@ export const updateRetentionPolicyAsync = (database, retentionPolicy, updates) =
dispatch(publishAutoDismissingNotification('success', 'Retention policy updated successfully'))
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update retention policy: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update retention policy: ${error.data.message}`))
}
}
export const killQueryAsync = (source, queryID) => (dispatch) => {
export const killQueryAsync = (source, queryID) => async (dispatch) => {
// optimistic update
dispatch(killQuery(queryID))
dispatch(setQueryToKill(null))
// kill query on server
killQueryProxy(source, queryID)
try {
// kill query on server
await killQueryProxy(source, queryID)
} catch (error) {
dispatch(errorThrown(error))
// TODO: handle failed killQuery
}
}
export const deleteRoleAsync = (role) => async (dispatch) => {
@ -314,7 +334,7 @@ export const deleteRoleAsync = (role) => async (dispatch) => {
await deleteRoleAJAX(role.links.self)
dispatch(publishAutoDismissingNotification('success', 'Role deleted'))
} catch (error) {
dispatch(publishNotification('error', `Failed to delete role: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to delete role: ${error.data.message}`))
}
}
@ -324,7 +344,7 @@ export const deleteUserAsync = (user) => async (dispatch) => {
await deleteUserAJAX(user.links.self)
dispatch(publishAutoDismissingNotification('success', 'User deleted'))
} catch (error) {
dispatch(publishNotification('error', `Failed to delete user: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to delete user: ${error.data.message}`))
}
}
@ -334,7 +354,7 @@ export const deleteDatabaseAsync = (database) => async (dispatch) => {
await deleteDatabaseAJAX(database.links.self)
dispatch(publishAutoDismissingNotification('success', 'Database deleted'))
} catch (error) {
dispatch(publishNotification('error', `Failed to delete database: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to delete database: ${error.data.message}`))
}
}
@ -344,7 +364,7 @@ export const deleteRetentionPolicyAsync = (database, retentionPolicy) => async (
await deleteRetentionPolicyAJAX(retentionPolicy.links.self)
dispatch(publishAutoDismissingNotification('success', `Retention policy ${retentionPolicy.name} deleted`))
} catch (error) {
dispatch(publishNotification('error', `Failed to delete retentionPolicy: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to delete retentionPolicy: ${error.data.message}`))
}
}
@ -354,7 +374,7 @@ export const updateRoleUsersAsync = (role, users) => async (dispatch) => {
dispatch(publishAutoDismissingNotification('success', 'Role users updated'))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update role: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`))
}
}
@ -364,7 +384,7 @@ export const updateRolePermissionsAsync = (role, permissions) => async (dispatch
dispatch(publishAutoDismissingNotification('success', 'Role permissions updated'))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update role: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`))
}
}
@ -374,7 +394,7 @@ export const updateUserPermissionsAsync = (user, permissions) => async (dispatch
dispatch(publishAutoDismissingNotification('success', 'User permissions updated'))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
}
}
@ -384,7 +404,7 @@ export const updateUserRolesAsync = (user, roles) => async (dispatch) => {
dispatch(publishAutoDismissingNotification('success', 'User roles updated'))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
}
}
@ -394,6 +414,6 @@ export const updateUserPasswordAsync = (user, password) => async (dispatch) => {
dispatch(publishAutoDismissingNotification('success', 'User password updated'))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
}
}

View File

@ -37,7 +37,7 @@ const RoleRow = ({
<RoleEditingRow role={role} onEdit={onEdit} onSave={onSave} isNew={isNew} />
<td></td>
<td></td>
<td className="text-right" style={{width: "85px"}}>
<td className="text-right" style={{width: '85px'}}>
<ConfirmButtons item={role} onConfirm={onSave} onCancel={onCancel} />
</td>
</tr>

View File

@ -42,7 +42,7 @@ const UserRow = ({
<UserEditingRow user={user} onEdit={onEdit} onSave={onSave} isNew={isNew} />
{hasRoles ? <td></td> : null}
<td></td>
<td className="text-right" style={{width: "85px"}}>
<td className="text-right" style={{width: '85px'}}>
<ConfirmButtons item={user} onConfirm={onSave} onCancel={onCancel} />
</td>
</tr>
@ -75,7 +75,7 @@ const UserRow = ({
/> : null
}
</td>
<td className="text-right" style={{width: "300px"}}>
<td className="text-right" style={{width: '300px'}}>
<ChangePassRow onEdit={onEdit} onApply={handleUpdatePassword} user={user} />
</td>
<DeleteConfirmTableCell onDelete={onDelete} item={user} />

View File

@ -4,6 +4,6 @@ export function getAlerts(source, timeRange) {
return proxy({
source,
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc`,
db: "chronograf",
db: 'chronograf',
})
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import {replace} from 'react-router-redux'
import {UserAuthWrapper} from 'redux-auth-wrapper'
export const UserIsAuthenticated = UserAuthWrapper({
authSelector: ({auth}) => ({auth}),
authenticatingSelector: ({auth: {isMeLoading}}) => isMeLoading,
LoadingComponent: (() => <div className="page-spinner" />),
redirectAction: replace,
wrapperDisplayName: 'UserIsAuthenticated',
predicate: ({auth: {me, isMeLoading}}) => !isMeLoading && me !== null,
})
export const UserIsNotAuthenticated = UserAuthWrapper({
authSelector: ({auth}) => ({auth}),
authenticatingSelector: ({auth: {isMeLoading}}) => isMeLoading,
LoadingComponent: (() => <div className="page-spinner" />),
redirectAction: replace,
wrapperDisplayName: 'UserIsNotAuthenticated',
predicate: ({auth: {me, isMeLoading}}) => !isMeLoading && me === null,
failureRedirectPath: () => '/',
allowRedirectBack: false,
})

View File

@ -1,33 +1,51 @@
/* global VERSION */
import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
const {array} = PropTypes
import Notifications from 'shared/components/Notifications'
const Login = ({auth}) => (
<div className="auth-page">
<div className="auth-box">
<div className="auth-logo"></div>
<h1 className="auth-text-logo">Chronograf</h1>
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p>
{auth.map(({name, login, label}) => (
<a key={name} className="btn btn-primary" href={login}>
<span className={`icon ${name}`}></span>
Login with {label}
</a>
))}
const Login = ({authData: {auth}}) => {
if (auth.isAuthLoading) {
return <div className="page-spinner"></div>
}
return (
<div>
<Notifications />
<div className="auth-page">
<div className="auth-box">
<div className="auth-logo"></div>
<h1 className="auth-text-logo">Chronograf</h1>
<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}>
<span className={`icon ${name}`}></span>
Login with {label}
</a>
))}
</div>
<p className="auth-credits">Made by <span className="icon cubo-uniform"></span>InfluxData</p>
<div className="auth-image"></div>
</div>
</div>
<p className="auth-credits">Made by <span className="icon cubo-uniform"></span>InfluxData</p>
<div className="auth-image"></div>
</div>
)
Login.propTypes = {
auth: array.isRequired,
)
}
const mapStateToProps = (state) => ({
auth: state.auth,
})
const {
array,
bool,
shape,
string,
} = PropTypes
export default connect(mapStateToProps)(Login)
Login.propTypes = {
authData: shape({
me: shape(),
links: array,
isLoading: bool,
}),
location: shape({
pathname: string,
}),
}
export default Login

View File

@ -1,2 +1,3 @@
import Login from './Login'
export {Login}
import {UserIsAuthenticated, Authenticated, UserIsNotAuthenticated} from './Authenticated'
export {Login, UserIsAuthenticated, Authenticated, UserIsNotAuthenticated}

View File

@ -7,9 +7,8 @@ import {
deleteDashboardCell as deleteDashboardCellAJAX,
} from 'src/dashboards/apis'
import {publishNotification} from 'shared/actions/notifications'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
// import {errorThrown} from 'shared/actions/errors'
import {errorThrown} from 'shared/actions/errors'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
@ -138,6 +137,7 @@ export const getDashboardsAsync = () => async dispatch => {
const {data: {dashboards}} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
} catch (error) {
dispatch(errorThrown(error))
console.error(error)
throw error
}
@ -159,7 +159,7 @@ export const updateDashboardCell = (dashboard, cell) => async dispatch => {
dispatch(syncDashboardCell(dashboard, data))
} catch (error) {
console.error(error)
// dispatch(errorThrown(error))
dispatch(errorThrown(error))
}
}
@ -174,13 +174,10 @@ export const deleteDashboardAsync = dashboard => async dispatch => {
)
)
} catch (error) {
dispatch(deleteDashboardFailed(dashboard))
dispatch(
publishNotification(
'error',
`Failed to delete dashboard: ${error.data.message}.`
)
errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`)
)
dispatch(deleteDashboardFailed(dashboard))
}
}
@ -192,6 +189,7 @@ export const addDashboardCellAsync = dashboard => async dispatch => {
)
dispatch(addDashboardCell(dashboard, data))
} catch (error) {
dispatch(errorThrown(error))
console.error(error)
throw error
}
@ -202,7 +200,7 @@ export const deleteDashboardCellAsync = cell => async dispatch => {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(cell))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
throw error
}
}

View File

@ -3,8 +3,10 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import uuid from 'node-uuid'
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
import QueryBuilder from 'src/data_explorer/components/QueryBuilder'
import ResizeContainer, {
ResizeBottom,
} from 'src/shared/components/ResizeContainer'
import QueryMaker from 'src/data_explorer/components/QueryMaker'
import Visualization from 'src/data_explorer/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import * as queryModifiers from 'src/utils/queryTransitions'
@ -30,7 +32,9 @@ class CellEditorOverlay extends Component {
const {cell: {name, type, queries}} = props
const queriesWorkingDraft = _.cloneDeep(queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})))
const queriesWorkingDraft = _.cloneDeep(
queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()}))
)
this.state = {
cellWorkingName: name,
@ -45,7 +49,9 @@ class CellEditorOverlay extends Component {
const nextStatus = nextProps.queryStatus
if (nextStatus.status && nextStatus.queryID) {
if (nextStatus.queryID !== queryID || nextStatus.status !== status) {
const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === queryID ? ({...q, status: nextStatus.status}) : q)
const nextQueries = this.state.queriesWorkingDraft.map(
q => (q.id === queryID ? {...q, status: nextStatus.status} : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
}
@ -54,11 +60,13 @@ class CellEditorOverlay extends Component {
queryStateReducer(queryModifier) {
return (queryID, payload) => {
const {queriesWorkingDraft} = this.state
const query = queriesWorkingDraft.find((q) => q.id === queryID)
const query = queriesWorkingDraft.find(q => q.id === queryID)
const nextQuery = queryModifier(query, payload)
const nextQueries = queriesWorkingDraft.map((q) => q.id === query.id ? nextQuery : q)
const nextQueries = queriesWorkingDraft.map(
q => (q.id === query.id ? nextQuery : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
}
@ -70,7 +78,9 @@ class CellEditorOverlay extends Component {
}
handleDeleteQuery(index) {
const nextQueries = this.state.queriesWorkingDraft.filter((__, i) => i !== index)
const nextQueries = this.state.queriesWorkingDraft.filter(
(__, i) => i !== index
)
this.setState({queriesWorkingDraft: nextQueries})
}
@ -81,11 +91,9 @@ class CellEditorOverlay extends Component {
const newCell = _.cloneDeep(cell)
newCell.name = cellWorkingName
newCell.type = cellWorkingType
newCell.queries = queriesWorkingDraft.map((q) => {
newCell.queries = queriesWorkingDraft.map(q => {
const query = q.rawText || buildInfluxQLQuery(timeRange, q)
const label = q.rawText ?
"" :
`${q.measurement}.${q.fields[0].field}`
const label = q.rawText ? '' : `${q.measurement}.${q.fields[0].field}`
return {
queryConfig: q,
@ -110,7 +118,9 @@ class CellEditorOverlay extends Component {
try {
const {data} = await getQueryConfig(url, [{query: text, id}])
const config = data.queries.find(q => q.id === id)
const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === id ? config.queryConfig : q)
const nextQueries = this.state.queriesWorkingDraft.map(
q => (q.id === id ? config.queryConfig : q)
)
this.setState({queriesWorkingDraft: nextQueries})
} catch (error) {
console.error(error)
@ -136,7 +146,7 @@ class CellEditorOverlay extends Component {
const queryActions = {
addQuery: this.handleAddQuery,
editRawTextAsync: this.handleEditRawText,
..._.mapValues(queryModifiers, (qm) => this.queryStateReducer(qm)),
..._.mapValues(queryModifiers, qm => this.queryStateReducer(qm)),
}
return (
@ -150,16 +160,19 @@ class CellEditorOverlay extends Component {
cellType={cellWorkingType}
cellName={cellWorkingName}
editQueryStatus={editQueryStatus}
views={[]}
/>
<ResizeBottom>
<div style={{display: 'flex', flexDirection: 'column', height: '100%'}}>
<div
style={{display: 'flex', flexDirection: 'column', height: '100%'}}
>
<OverlayControls
selectedGraphType={cellWorkingType}
onSelectGraphType={this.handleSelectGraphType}
onCancel={onCancel}
onSave={this.handleSaveCell}
/>
<QueryBuilder
<QueryMaker
source={source}
queries={queriesWorkingDraft}
actions={queryActions}
@ -177,12 +190,7 @@ class CellEditorOverlay extends Component {
}
}
const {
func,
number,
shape,
string,
} = PropTypes
const {func, number, shape, string} = PropTypes
CellEditorOverlay.propTypes = {
onCancel: func.isRequired,

View File

@ -9,7 +9,7 @@ const OverlayControls = (props) => {
const {onCancel, onSave, selectedGraphType, onSelectGraphType} = props
return (
<div className="overlay-controls">
<h3 className="overlay--graph-name">Graph Editor</h3>
<h3 className="overlay--graph-name">Cell Editor</h3>
<div className="overlay-controls--right">
<p>Visualization Type:</p>
<ul className="toggle toggle-sm">

View File

@ -56,9 +56,9 @@ const DashboardsPage = React.createClass({
const dashboardLink = `/sources/${this.props.source.id}`
let tableHeader
if (dashboards === null) {
tableHeader = "Loading Dashboards..."
tableHeader = 'Loading Dashboards...'
} else if (dashboards.length === 0) {
tableHeader = "1 Dashboard"
tableHeader = '1 Dashboard'
} else {
tableHeader = `${dashboards.length + 1} Dashboards`
}

View File

@ -1,6 +1,9 @@
import uuid from 'node-uuid'
import {getQueryConfig} from 'shared/apis'
import {errorThrown} from 'shared/actions/errors'
export function addQuery(options = {}) {
return {
type: 'ADD_QUERY',
@ -137,15 +140,13 @@ export const updateQueryConfig = (config) => ({
},
})
export function editQueryStatus(queryID, status) {
return {
type: 'EDIT_QUERY_STATUS',
payload: {
queryID,
status,
},
}
}
export const editQueryStatus = (queryID, status) => ({
type: 'EDIT_QUERY_STATUS',
payload: {
queryID,
status,
},
})
// Async actions
export const editRawTextAsync = (url, id, text) => async (dispatch) => {
@ -154,6 +155,6 @@ export const editRawTextAsync = (url, id, text) => async (dispatch) => {
const config = data.queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}

View File

@ -61,19 +61,19 @@ const DatabaseList = React.createClass({
return (
<div className="query-builder--column">
<div className="query-builder--column-heading">Databases</div>
<ul className="qeditor--list">
<div className="query-builder--heading">Databases</div>
<div className="query-builder--list">
{this.state.namespaces.map((namespace) => {
const {database, retentionPolicy} = namespace
const isActive = database === query.database && retentionPolicy === query.retentionPolicy
return (
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
<div className={classNames('query-builder--list-item', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
{database}.{retentionPolicy}
</li>
</div>
)
})}
</ul>
</div>
</div>
)
},

View File

@ -74,17 +74,12 @@ const FieldList = React.createClass({
return (
<div className="query-builder--column">
<div className="query-builder--column-heading">Fields</div>
{
hasAggregates ?
<div className="qeditor--list-header">
<div className="group-by-time">
<p>Group by Time</p>
<GroupByTimeDropdown isOpen={!hasGroupByTime} selected={query.groupBy.time} onChooseGroupByTime={this.handleGroupByTime} />
</div>
</div>
: null
}
<div className="query-builder--heading">
<span>Fields</span>
{hasAggregates ?
<GroupByTimeDropdown isOpen={!hasGroupByTime} selected={query.groupBy.time} onChooseGroupByTime={this.handleGroupByTime} />
: null}
</div>
{this.renderList()}
</div>
)
@ -93,11 +88,15 @@ const FieldList = React.createClass({
renderList() {
const {database, measurement} = this.props.query
if (!database || !measurement) {
return <div className="qeditor--empty">No <strong>Measurement</strong> selected</div>
return (
<div className="query-builder--list-empty">
<span>No <strong>Measurement</strong> selected</span>
</div>
)
}
return (
<ul className="qeditor--list">
<div className="query-builder--list">
{this.state.fields.map((fieldFunc) => {
const selectedField = this.props.query.fields.find((f) => f.field === fieldFunc.field)
return (
@ -111,7 +110,7 @@ const FieldList = React.createClass({
/>
)
})}
</ul>
</div>
)
},

View File

@ -39,16 +39,17 @@ const FieldListItem = React.createClass({
})
return (
<li className={classNames("qeditor--list-item qeditor--list-checkbox", {checked: isSelected})} key={fieldFunc} onClick={_.wrap(fieldFunc, this.handleToggleField)}>
<span className="qeditor--list-checkbox__checkbox">{fieldText}</span>
<div className="qeditor--hidden-dropdown">
{
isKapacitorRule ?
<Dropdown items={items} onChoose={this.handleApplyFunctions} selected={fieldFunc.funcs.length ? fieldFunc.funcs[0] : 'Select a function'} /> :
<MultiSelectDropdown items={INFLUXQL_FUNCTIONS} onApply={this.handleApplyFunctions} selectedItems={fieldFunc.funcs || []} />
}
</div>
</li>
<div className={classNames('query-builder--list-item', {active: isSelected})} key={fieldFunc} onClick={_.wrap(fieldFunc, this.handleToggleField)}>
<span>
<div className="query-builder--checkbox"></div>
{fieldText}
</span>
{
isKapacitorRule ?
<Dropdown items={items} onChoose={this.handleApplyFunctions} selected={fieldFunc.funcs.length ? fieldFunc.funcs[0] : 'Function'} /> :
<MultiSelectDropdown items={INFLUXQL_FUNCTIONS} onApply={this.handleApplyFunctions} selectedItems={fieldFunc.funcs || []} />
}
</div>
)
},
})

View File

@ -16,16 +16,16 @@ const GroupByTimeDropdown = React.createClass({
const {isOpen, selected, onChooseGroupByTime} = this.props
return (
<div className="dropdown group-by-time-dropdown">
<div className={classNames('dropdown group-by-time-dropdown', {open: isOpen})}>
<div className="btn btn-sm btn-info dropdown-toggle" data-toggle="dropdown">
<span className="selected-group-by">{selected || '...'}</span>
<span className="caret" />
<span>Group by {selected || 'time'}</span>
<span className="caret"></span>
</div>
<ul className={classNames("dropdown-menu", {show: isOpen})} aria-labelledby="group-by-dropdown">
<ul className="dropdown-menu">
{groupByTimeOptions.map((groupBy) => {
return (
<li key={groupBy.menuOption}>
<a href="#" onClick={() => onChooseGroupByTime(groupBy)}>
<li className="dropdown-item" key={groupBy.menuOption}onClick={() => onChooseGroupByTime(groupBy)}>
<a href="#">
{groupBy.menuOption}
</a>
</li>

View File

@ -25,7 +25,7 @@ const MeasurementList = React.createClass({
getInitialState() {
return {
measurements: [],
filterText: "",
filterText: '',
}
},
@ -72,11 +72,15 @@ const MeasurementList = React.createClass({
render() {
return (
<div className="query-builder--column">
<div className="query-builder--column-heading">Measurements</div>
{this.props.query.database ? <div className="qeditor--list-header">
<input className="qeditor--filter" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
<span className="icon search"></span>
</div> : null }
<div className="query-builder--heading">
<span>Measurements</span>
{this.props.query.database ?
<div className="query-builder--filter">
<input className="form-control input-sm" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
<span className="icon search"></span>
</div>
: null }
</div>
{this.renderList()}
</div>
)
@ -84,20 +88,24 @@ const MeasurementList = React.createClass({
renderList() {
if (!this.props.query.database) {
return <div className="qeditor--empty">No <strong>Database</strong> selected</div>
return (
<div className="query-builder--list-empty">
<span>No <strong>Database</strong> selected</span>
</div>
)
}
const measurements = this.state.measurements.filter((m) => m.match(this.state.filterText))
return (
<ul className="qeditor--list">
<div className="query-builder--list">
{measurements.map((measurement) => {
const isActive = measurement === this.props.query.measurement
return (
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={measurement} onClick={_.wrap(measurement, this.props.onChooseMeasurement)}>{measurement}</li>
<div className={classNames('query-builder--list-item', {active: isActive})} key={measurement} onClick={_.wrap(measurement, this.props.onChooseMeasurement)}>{measurement}</div>
)
})}
</ul>
</div>
)
},

View File

@ -1,16 +1,16 @@
import React, {PropTypes} from 'react'
import DatabaseList from './DatabaseList'
import MeasurementList from './MeasurementList'
import FieldList from './FieldList'
import TagList from './TagList'
import QueryEditor from './QueryEditor'
import QueryTabItem from './QueryTabItem'
import buildInfluxQLQuery from 'utils/influxql'
const {
arrayOf,
func,
node,
number,
shape,
string,
shape,
func,
} = PropTypes
const QueryBuilder = React.createClass({
@ -20,7 +20,9 @@ const QueryBuilder = React.createClass({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
query: shape({
id: string,
}).isRequired,
timeRange: shape({
upper: string,
lower: string,
@ -28,103 +30,91 @@ const QueryBuilder = React.createClass({
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
addQuery: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
height: string,
top: string,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
children: node,
},
handleAddQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery()
this.props.setActiveQueryIndex(newIndex)
handleChooseNamespace(namespace) {
this.props.actions.chooseNamespace(this.props.query.id, namespace)
},
handleAddRawQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery({rawText: ''})
this.props.setActiveQueryIndex(newIndex)
handleChooseMeasurement(measurement) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
},
getActiveQuery() {
const {queries, activeQueryIndex} = this.props
const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0]
handleToggleField(field) {
this.props.actions.toggleField(this.props.query.id, field)
},
return activeQuery || defaultQuery
handleGroupByTime(time) {
this.props.actions.groupByTime(this.props.query.id, time)
},
handleApplyFuncsToField(fieldFunc) {
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc)
},
handleChooseTag(tag) {
this.props.actions.chooseTag(this.props.query.id, tag)
},
handleToggleTagAcceptance() {
this.props.actions.toggleTagAcceptance(this.props.query.id)
},
handleGroupByTag(tagKey) {
this.props.actions.groupByTag(this.props.query.id, tagKey)
},
handleEditRawText(text) {
const {source: {links}, query} = this.props
this.props.actions.editRawTextAsync(links.queries, query.id, text)
},
render() {
const {height, top} = this.props
const {query, timeRange} = this.props
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
return (
<div className="query-builder" style={{height, top}}>
{this.renderQueryTabList()}
{this.renderQueryEditor()}
<div className="query-maker--tab-contents">
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
{this.renderLists()}
</div>
)
},
renderQueryEditor() {
const {timeRange, actions, source} = this.props
const query = this.getActiveQuery()
if (!query) {
return (
<div className="qeditor--empty">
<h5 className="no-user-select">This Graph has no Queries</h5>
<br/>
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
</div>
)
}
renderLists() {
const {query} = this.props
return (
<QueryEditor
source={source}
timeRange={timeRange}
query={query}
actions={actions}
onAddQuery={this.handleAddQuery}
/>
)
},
renderQueryTabList() {
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props
return (
<div className="query-builder--tabs">
<div className="query-builder--tabs-heading">
<h1>Queries</h1>
<div className="panel--tab-new btn btn-sm btn-primary dropdown-toggle" onClick={this.handleAddQuery}>
<span className="icon plus"></span>
</div>
</div>
{queries.map((q, i) => {
return (
<QueryTabItem
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`}
/>
)
})}
{this.props.children}
<div className="query-builder">
<DatabaseList
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
/>
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
/>
<TagList
query={query}
onChooseTag={this.handleChooseTag}
onGroupByTag={this.handleGroupByTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
/>
</div>
)
},

View File

@ -1,122 +1,120 @@
import React, {PropTypes} from 'react'
import classNames from 'classnames'
import Dropdown from 'src/shared/components/Dropdown'
import LoadingDots from 'src/shared/components/LoadingDots'
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
import DatabaseList from './DatabaseList'
import MeasurementList from './MeasurementList'
import FieldList from './FieldList'
import TagList from './TagList'
import RawQueryEditor from './RawQueryEditor'
import buildInfluxQLQuery from 'utils/influxql'
const {
string,
shape,
func,
} = PropTypes
const ENTER = 13
const ESCAPE = 27
const {bool, func, shape, string} = PropTypes
const QueryEditor = React.createClass({
propTypes: {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
query: shape({
id: string,
}).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
editRawTextAsync: func.isRequired,
query: string.isRequired,
onUpdate: func.isRequired,
config: shape({
status: shape({
error: string,
loading: bool,
success: string,
warn: string,
}),
}).isRequired,
},
handleChooseNamespace(namespace) {
this.props.actions.chooseNamespace(this.props.query.id, namespace)
getInitialState() {
return {
value: this.props.query,
}
},
handleChooseMeasurement(measurement) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
componentWillReceiveProps(nextProps) {
if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
},
handleToggleField(field) {
this.props.actions.toggleField(this.props.query.id, field)
handleKeyDown(e) {
if (e.keyCode === ENTER) {
e.preventDefault()
this.handleUpdate()
} else if (e.keyCode === ESCAPE) {
this.setState({value: this.state.value}, () => {
this.editor.blur()
})
}
},
handleGroupByTime(time) {
this.props.actions.groupByTime(this.props.query.id, time)
handleChange() {
this.setState({
value: this.editor.value,
})
},
handleApplyFuncsToField(fieldFunc) {
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc)
handleUpdate() {
this.props.onUpdate(this.state.value)
},
handleChooseTag(tag) {
this.props.actions.chooseTag(this.props.query.id, tag)
},
handleToggleTagAcceptance() {
this.props.actions.toggleTagAcceptance(this.props.query.id)
},
handleGroupByTag(tagKey) {
this.props.actions.groupByTag(this.props.query.id, tagKey)
},
handleEditRawText(text) {
const {source: {links}, query} = this.props
this.props.actions.editRawTextAsync(links.queries, query.id, text)
handleChooseTemplate(template) {
this.setState({value: template.query})
},
render() {
const {query, timeRange} = this.props
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
const {config: {status}} = this.props
const {value} = this.state
return (
<div className="query-builder--tab-contents">
<div>
<RawQueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
{this.renderLists()}
</div>
<div className="query-editor">
<textarea
className="query-editor--field"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleUpdate}
ref={editor => (this.editor = editor)}
value={value}
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
spellCheck="false"
/>
{this.renderStatus(status)}
<Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseTemplate}
className="query-editor--templates"
/>
</div>
)
},
renderStatus(status) {
if (!status) {
return <div className="query-editor--status" />
}
renderLists() {
const {query} = this.props
if (status.loading) {
return (
<div className="query-editor--status">
<LoadingDots />
</div>
)
}
return (
<div className="query-builder--columns">
<DatabaseList
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
/>
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
/>
<TagList
query={query}
onChooseTag={this.handleChooseTag}
onGroupByTag={this.handleGroupByTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
<div
className={classNames('query-editor--status', {
'query-editor--error': status.error,
'query-editor--success': status.success,
'query-editor--warning': status.warn,
})}
>
<span
className={classNames('icon', {
stop: status.error,
checkmark: status.success,
'alert-triangle': status.warn,
})}
/>
{status.error || status.warn || status.success}
</div>
)
},

View File

@ -0,0 +1,130 @@
import React, {PropTypes} from 'react'
import QueryBuilder from './QueryBuilder'
import QueryMakerTab from './QueryMakerTab'
import buildInfluxQLQuery from 'utils/influxql'
const {
arrayOf,
func,
node,
number,
shape,
string,
} = PropTypes
const QueryMaker = React.createClass({
propTypes: {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
addQuery: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
height: string,
top: string,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
children: node,
},
handleAddQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery()
this.props.setActiveQueryIndex(newIndex)
},
handleAddRawQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery({rawText: ''})
this.props.setActiveQueryIndex(newIndex)
},
getActiveQuery() {
const {queries, activeQueryIndex} = this.props
const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0]
return activeQuery || defaultQuery
},
render() {
const {height, top} = this.props
return (
<div className="query-maker" style={{height, top}}>
{this.renderQueryTabList()}
{this.renderQueryBuilder()}
</div>
)
},
renderQueryBuilder() {
const {timeRange, actions, source} = this.props
const query = this.getActiveQuery()
if (!query) {
return (
<div className="query-maker--empty">
<h5>This Graph has no Queries</h5>
<br/>
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
</div>
)
}
return (
<QueryBuilder
source={source}
timeRange={timeRange}
query={query}
actions={actions}
onAddQuery={this.handleAddQuery}
/>
)
},
renderQueryTabList() {
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props
return (
<div className="query-maker--tabs">
{queries.map((q, i) => {
return (
<QueryMakerTab
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`}
/>
)
})}
{this.props.children}
<div className="query-maker--new btn btn-sm btn-primary" onClick={this.handleAddQuery}>
<span className="icon plus"></span>
</div>
</div>
)
},
})
export default QueryMaker

View File

@ -1,7 +1,7 @@
import React, {PropTypes} from 'react'
import classNames from 'classnames'
const QueryTabItem = React.createClass({
const QueryMakerTab = React.createClass({
propTypes: {
isActive: PropTypes.bool,
query: PropTypes.shape({
@ -24,12 +24,12 @@ const QueryTabItem = React.createClass({
render() {
return (
<div className={classNames('query-builder--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
<span className="query-builder--tab-label">{this.props.queryTabText}</span>
<span className="query-builder--tab-delete" onClick={this.handleDelete}></span>
<div className={classNames('query-maker--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
<label>{this.props.queryTabText}</label>
<span className="query-maker--delete" onClick={this.handleDelete}></span>
</div>
)
},
})
export default QueryTabItem
export default QueryMakerTab

View File

@ -1,105 +0,0 @@
import React, {PropTypes} from 'react'
import classNames from 'classnames'
import Dropdown from 'src/shared/components/Dropdown'
import LoadingDots from 'src/shared/components/LoadingDots'
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
const ENTER = 13
const ESCAPE = 27
const {
func,
shape,
string,
} = PropTypes
const RawQueryEditor = React.createClass({
propTypes: {
query: string.isRequired,
onUpdate: func.isRequired,
config: shape().isRequired,
},
getInitialState() {
return {
value: this.props.query,
}
},
componentWillReceiveProps(nextProps) {
if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
},
handleKeyDown(e) {
if (e.keyCode === ENTER) {
e.preventDefault()
this.handleUpdate()
} else if (e.keyCode === ESCAPE) {
this.setState({value: this.state.value}, () => {
this.editor.blur()
})
}
},
handleChange() {
this.setState({
value: this.editor.value,
})
},
handleUpdate() {
this.props.onUpdate(this.state.value)
},
handleChooseTemplate(template) {
this.setState({value: template.query})
},
render() {
const {config: {status}} = this.props
const {value} = this.state
return (
<div className="raw-text">
<textarea
className="raw-text--field"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleUpdate}
ref={(editor) => this.editor = editor}
value={value}
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
spellCheck="false"
/>
{this.renderStatus(status)}
<Dropdown items={QUERY_TEMPLATES} selected={'Query Templates'} onChoose={this.handleChooseTemplate} className="query-template"/>
</div>
)
},
renderStatus(status) {
if (!status) {
return (
<div className="raw-text--status"></div>
)
}
if (status.loading) {
return (
<div className="raw-text--status">
<LoadingDots />
</div>
)
}
return (
<div className={classNames("raw-text--status", {"raw-text--error": status.error, "raw-text--success": status.success, "raw-text--warning": status.warn})}>
<span className={classNames("icon", {stop: status.error, checkmark: status.success, "alert-triangle": status.warn})}></span>
{status.error || status.warn || status.success}
</div>
)
},
})
export default RawQueryEditor

View File

@ -1,10 +1,13 @@
import React, {PropTypes} from 'react'
import {Table, Column, Cell} from 'fixed-data-table'
import Dimensions from 'react-dimensions'
import _ from 'lodash'
import moment from 'moment'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {Table, Column, Cell} from 'fixed-data-table'
const {
arrayOf,
func,
@ -105,7 +108,6 @@ const ChronoTable = React.createClass({
isLoading: false,
cellData: emptyCells,
})
console.error(error)
throw error
}
},

View File

@ -91,13 +91,16 @@ const TagList = React.createClass({
return (
<div className="query-builder--column">
<div className="query-builder--column-heading">Tags</div>
{(!query.database || !query.measurement || !query.retentionPolicy) ? null : <div className="qeditor--list-header">
<div className="toggle toggle-sm">
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: query.areTagsAccepted})}>=</div>
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: !query.areTagsAccepted})}>!=</div>
</div>
</div>}
<div className="query-builder--heading">
<span>Tags</span>
{(!query.database || !query.measurement || !query.retentionPolicy) ? null :
<div className={cx('flip-toggle', {flipped: query.areTagsAccepted})} onClick={this.handleAcceptReject}>
<div className="flip-toggle--container">
<div className="flip-toggle--front">!=</div>
<div className="flip-toggle--back">=</div>
</div>
</div>}
</div>
{this.renderList()}
</div>
)
@ -106,11 +109,15 @@ const TagList = React.createClass({
renderList() {
const {database, measurement, retentionPolicy} = this.props.query
if (!database || !measurement || !retentionPolicy) {
return <div className="qeditor--empty">No <strong>Measurement</strong> selected</div>
return (
<div className="query-builder--list-empty">
<span>No <strong>Measurement</strong> selected</span>
</div>
)
}
return (
<ul className="qeditor--list">
<div className="query-builder--list">
{_.map(this.state.tags, (tagValues, tagKey) => {
return (
<TagListItem
@ -124,7 +131,7 @@ const TagList = React.createClass({
/>
)
})}
</ul>
</div>
)
},
})

View File

@ -55,23 +55,23 @@ const TagListItem = React.createClass({
const filtered = tagValues.filter((v) => v.match(this.state.filterText))
return (
<li>
<div className="tag-value-list__filter-container">
<input className="tag-value-list__filter" ref="filterText" placeholder={`Filter within ${this.props.tagKey}`} type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
<div className="query-builder--sub-list">
<div className="query-builder--filter">
<input className="form-control input-sm" ref="filterText" placeholder={`Filter within ${this.props.tagKey}`} type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
<span className="icon search"></span>
</div>
<ul className="tag-value-list">
{filtered.map((v) => {
const cx = classNames('tag-value-list__item qeditor--list-item', {active: selectedTagValues.indexOf(v) > -1})
return (
<li className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
<div className="tag-value-list__checkbox"></div>
<div className="tag-value-list__item-label">{v}</div>
</li>
)
})}
</ul>
</li>
{filtered.map((v) => {
const cx = classNames('query-builder--list-item', {active: selectedTagValues.indexOf(v) > -1})
return (
<div className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
<span>
<div className="query-builder--checkbox"></div>
{v}
</span>
</div>
)
})}
</div>
)
},
@ -83,23 +83,20 @@ const TagListItem = React.createClass({
render() {
const {tagKey, tagValues} = this.props
const {isOpen} = this.state
const itemClasses = classNames("qeditor--list-item tag-list__item", {open: isOpen})
const tagItemLabel = `${tagKey}${tagValues.length}`
return (
<div>
<li className={itemClasses} onClick={this.handleClickKey}>
<div className="tag-list__title">
<div className="tag-list__caret">
<div className="icon caret-right"></div>
</div>
{tagKey}
<span className="badge">{tagValues.length}</span>
</div>
<div className={classNames('query-builder--list-item', {active: isOpen})} onClick={this.handleClickKey}>
<span>
<div className="query-builder--caret icon caret-right"></div>
{tagItemLabel}
</span>
<div
className={classNames('btn btn-info btn-xs tag-list__group-by', {active: this.props.isUsingGroupBy})}
className={classNames('btn btn-info btn-xs group-by-tag', {active: this.props.isUsingGroupBy})}
onClick={this.handleGroupBy}>Group By {tagKey}
</div>
</li>
</div>
{isOpen ? this.renderTagValues() : null}
</div>
)

View File

@ -4,26 +4,25 @@ import classNames from 'classnames'
const VisHeader = ({views, view, onToggleView, name}) => (
<div className="graph-heading">
<div className="graph-actions">
<ul className="toggle toggle-sm">
{views.map(v => (
<li
key={v}
onClick={() => onToggleView(v)}
className={classNames("toggle-btn ", {active: view === v})}>
{v}
</li>
))}
</ul>
{views.length
? <ul className="toggle toggle-sm">
{views.map(v => (
<li
key={v}
onClick={() => onToggleView(v)}
className={classNames('toggle-btn ', {active: view === v})}
>
{v}
</li>
))}
</ul>
: null}
</div>
<div className="graph-title">{name}</div>
</div>
)
const {
arrayOf,
func,
string,
} = PropTypes
const {arrayOf, func, string} = PropTypes
VisHeader.propTypes = {
views: arrayOf(string).isRequired,

View File

@ -49,7 +49,7 @@ const VisView = ({
autoRefresh={autoRefresh}
activeQueryIndex={activeQueryIndex}
isInDataExplorer={true}
showSingleStat={cellType === "line-plus-single-stat"}
showSingleStat={cellType === 'line-plus-single-stat'}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
/>

View File

@ -3,18 +3,9 @@ import buildInfluxQLQuery from 'utils/influxql'
import classNames from 'classnames'
import VisHeader from 'src/data_explorer/components/VisHeader'
import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'src/shared/constants'
const GRAPH = 'graph'
const TABLE = 'table'
const VIEWS = [GRAPH, TABLE]
const {
func,
arrayOf,
number,
shape,
string,
} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
const Visualization = React.createClass({
propTypes: {
@ -30,6 +21,7 @@ const Visualization = React.createClass({
height: string,
heightPixels: number,
editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
},
contextTypes: {
@ -49,13 +41,25 @@ const Visualization = React.createClass({
}
return {
view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH,
view: typeof queryConfigs[activeQueryIndex].rawText === 'string'
? TABLE
: GRAPH,
}
},
getDefaultProps() {
return {
cellName: '',
}
},
componentWillReceiveProps(nextProps) {
const {queryConfigs, activeQueryIndex} = nextProps
if (!queryConfigs.length || activeQueryIndex === null || activeQueryIndex === this.props.activeQueryIndex) {
if (
!queryConfigs.length ||
activeQueryIndex === null ||
activeQueryIndex === this.props.activeQueryIndex
) {
return
}
@ -71,8 +75,10 @@ const Visualization = React.createClass({
render() {
const {
views,
height,
cellType,
cellName,
timeRange,
autoRefresh,
heightPixels,
@ -83,18 +89,28 @@ const Visualization = React.createClass({
const {source: {links: {proxy}}} = this.context
const {view} = this.state
const statements = queryConfigs.map((query) => {
const statements = queryConfigs.map(query => {
const text = query.rawText || buildInfluxQLQuery(timeRange, query)
return {text, id: query.id}
})
const queries = statements.filter((s) => s.text !== null).map((s) => {
const queries = statements.filter(s => s.text !== null).map(s => {
return {host: [proxy], text: s.text, id: s.id}
})
return (
<div className="graph" style={{height}}>
<VisHeader views={VIEWS} view={view} onToggleView={this.handleToggleView} name={name || 'Graph'}/>
<div className={classNames({"graph-container": view === GRAPH, "table-container": view === TABLE})}>
<VisHeader
views={views}
view={view}
onToggleView={this.handleToggleView}
name={cellName}
/>
<div
className={classNames({
'graph-container': view === GRAPH,
'table-container': view === TABLE,
})}
>
<VisView
view={view}
queries={queries}

View File

@ -24,8 +24,8 @@ export const QUERY_TEMPLATES = [
{text: 'Create Continuous Query', query: 'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END'},
{text: 'Drop Continuous Query', query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"'},
{text: 'Show Users', query: 'SHOW USERS'},
{text: 'Create User', query: `CREATE USER "username" WITH PASSWORD 'password'`},
{text: 'Create Admin User', query: `CREATE USER "username" WITH PASSWORD 'password' WITH ALL PRIVILEGES`},
{text: 'Create User', query: 'CREATE USER "username" WITH PASSWORD \'password\''},
{text: 'Create Admin User', query: 'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES'},
{text: 'Drop User', query: 'DROP USER "username"'},
{text: 'Show Stats', query: 'SHOW STATS'},
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},

View File

@ -4,21 +4,18 @@ import {bindActionCreators} from 'redux'
import _ from 'lodash'
import QueryBuilder from '../components/QueryBuilder'
import QueryMaker from '../components/QueryMaker'
import Visualization from '../components/Visualization'
import Header from '../containers/Header'
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
import ResizeContainer, {
ResizeBottom,
} from 'src/shared/components/ResizeContainer'
import {VIS_VIEWS} from 'src/shared/constants'
import {setAutoRefresh} from 'shared/actions/app'
import * as viewActions from 'src/data_explorer/actions/view'
const {
arrayOf,
func,
number,
shape,
string,
} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
const DataExplorer = React.createClass({
propTypes: {
@ -94,7 +91,7 @@ const DataExplorer = React.createClass({
timeRange={timeRange}
/>
<ResizeContainer>
<QueryBuilder
<QueryMaker
source={source}
queries={queryConfigs}
actions={queryConfigActions}
@ -111,6 +108,7 @@ const DataExplorer = React.createClass({
queryConfigs={queryConfigs}
activeQueryIndex={activeQueryIndex}
editQueryStatus={queryConfigActions.editQueryStatus}
views={VIS_VIEWS}
/>
</ResizeBottom>
</ResizeContainer>
@ -120,7 +118,12 @@ const DataExplorer = React.createClass({
})
function mapStateToProps(state) {
const {app: {persisted: {autoRefresh}}, timeRange, queryConfigs, dataExplorer} = state
const {
app: {persisted: {autoRefresh}},
timeRange,
queryConfigs,
dataExplorer,
} = state
const queryConfigValues = _.values(queryConfigs)
return {

View File

@ -40,7 +40,7 @@ const Header = React.createClass({
const {autoRefresh, actions: {handleChooseAutoRefresh}, timeRange} = this.props
return (
<div className="page-header">
<div className="page-header full-width-no-scrollbar">
<div className="page-header__container">
<div className="page-header__left">
<h1>Data Explorer</h1>

View File

@ -5,7 +5,7 @@ import _ from 'lodash'
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
return proxy({
source: proxyLink,
query: `select mean(usage_user) from cpu where cpu = 'cpu-total' and time > now() - 10m group by host; select mean("load1") from "system" where time > now() - 10m group by host; select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); show tag values from /win_system|system/ with key = "host"`,
query: 'select mean(usage_user) from cpu where cpu = \'cpu-total\' and time > now() - 10m group by host; select mean("load1") from "system" where time > now() - 10m group by host; select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); show tag values from /win_system|system/ with key = "host"',
db: telegrafDB,
}).then((resp) => {
const hosts = {}
@ -88,6 +88,7 @@ export async function getAllHosts(proxyLink, telegrafDB) {
return hosts
} catch (error) {
console.error(error) // eslint-disable-line no-console
throw error
}
}

View File

@ -83,11 +83,11 @@ const HostsTable = React.createClass({
sortableClasses(key) {
if (this.state.sortKey === key) {
if (this.state.sortDirection === 'asc') {
return "sortable-header sorting-up"
return 'sortable-header sorting-up'
}
return "sortable-header sorting-down"
return 'sortable-header sorting-down'
}
return "sortable-header"
return 'sortable-header'
},
render() {
@ -99,11 +99,11 @@ const HostsTable = React.createClass({
let hostsTitle
if (hostsLoading) {
hostsTitle = `Loading Hosts...`
hostsTitle = 'Loading Hosts...'
} else if (hostsError.length) {
hostsTitle = `There was a problem loading hosts`
hostsTitle = 'There was a problem loading hosts'
} else if (hosts.length === 0) {
hostsTitle = `No hosts found`
hostsTitle = 'No hosts found'
} else if (hostCount === 1) {
hostsTitle = `${hostCount} Host`
} else {
@ -164,11 +164,11 @@ const HostRow = React.createClass({
const {host, source} = this.props
const {name, cpu, load, apps = []} = host
let stateStr = ""
let stateStr = ''
if (host.deltaUptime < 0) {
stateStr = "table-dot dot-critical"
stateStr = 'table-dot dot-critical'
} else if (host.deltaUptime > 0) {
stateStr = "table-dot dot-success"
stateStr = 'table-dot dot-success'
}
return (
@ -182,7 +182,7 @@ const HostRow = React.createClass({
return (
<span key={app}>
<Link
style={{marginLeft: "2px"}}
style={{marginLeft: '2px'}}
to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
{app}
</Link>

View File

@ -1,15 +1,16 @@
import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import {Router, Route, Redirect, useRouterHistory} from 'react-router'
import {Router, Route, useRouterHistory} from 'react-router'
import {createHistory} from 'history'
import {syncHistoryWithStore} from 'react-router-redux'
import App from 'src/App'
import AlertsApp from 'src/alerts'
import CheckSources from 'src/CheckSources'
import {HostsPage, HostPage} from 'src/hosts'
import {KubernetesPage} from 'src/kubernetes'
import {Login} from 'src/auth'
import {Login, UserIsAuthenticated, UserIsNotAuthenticated} from 'src/auth'
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor'
import DataExplorer from 'src/data_explorer'
import {DashboardsPage, DashboardPage} from 'src/dashboards'
@ -17,18 +18,18 @@ import {CreateSource, SourcePage, ManageSources} from 'src/sources'
import {AdminPage} from 'src/admin'
import NotFound from 'src/shared/components/NotFound'
import configureStore from 'src/store/configureStore'
import {getMe, getSources} from 'shared/apis'
import {receiveMe} from 'shared/actions/me'
import {receiveAuth} from 'shared/actions/auth'
import {disablePresentationMode} from 'shared/actions/app'
import {publishNotification} from 'shared/actions/notifications'
import {loadLocalStorage} from './localStorage'
import {getMe} from 'shared/apis'
import {disablePresentationMode} from 'shared/actions/app'
import {authRequested, authReceived, meRequested, meReceived} from 'shared/actions/auth'
import {errorThrown} from 'shared/actions/errors'
import 'src/style/chronograf.scss'
import {HTTP_FORBIDDEN, HEARTBEAT_INTERVAL} from 'shared/constants'
import {HEARTBEAT_INTERVAL} from 'shared/constants'
const store = configureStore(loadLocalStorage())
const rootNode = document.getElementById('react-root')
let browserHistory
@ -40,98 +41,66 @@ if (basepath) {
})
} else {
browserHistory = useRouterHistory(createHistory)({
basename: "",
basename: '',
})
}
const store = configureStore(loadLocalStorage(), browserHistory)
const {dispatch} = store
browserHistory.listen(() => {
store.dispatch(disablePresentationMode())
dispatch(disablePresentationMode())
})
window.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
store.dispatch(disablePresentationMode())
dispatch(disablePresentationMode())
}
})
const history = syncHistoryWithStore(browserHistory, store)
const Root = React.createClass({
getInitialState() {
return {
loggedIn: null,
}
},
componentDidMount() {
componentWillMount() {
this.checkAuth()
},
activeSource(sources) {
const defaultSource = sources.find((s) => s.default)
if (defaultSource && defaultSource.id) {
return defaultSource
async checkAuth() {
dispatch(authRequested())
dispatch(meRequested())
try {
await this.startHeartbeat({shouldDispatchResponse: true})
} catch (error) {
dispatch(errorThrown(error))
}
return sources[0]
},
redirectFromRoot(_, replace, callback) {
getSources().then(({data: {sources}}) => {
if (sources && sources.length) {
const path = `/sources/${this.activeSource(sources).id}/hosts`
replace(path)
}
callback()
})
},
checkAuth() {
if (store.getState().me.links) {
return this.setState({loggedIn: true})
}
this.heartbeat({shouldDispatchResponse: true})
},
async heartbeat({shouldDispatchResponse}) {
async startHeartbeat({shouldDispatchResponse}) {
try {
const {data: me, auth} = await getMe()
if (shouldDispatchResponse) {
store.dispatch(receiveMe(me))
store.dispatch(receiveAuth(auth))
this.setState({loggedIn: true})
dispatch(authReceived(auth))
dispatch(meReceived(me))
}
setTimeout(this.heartbeat.bind(null, {shouldDispatchResponse: false}), HEARTBEAT_INTERVAL)
setTimeout(() => {
if (store.getState().auth.me !== null) {
this.startHeartbeat({shouldDispatchResponse: false})
}
}, HEARTBEAT_INTERVAL)
} catch (error) {
if (error.auth) {
store.dispatch(receiveAuth(error.auth))
}
if (error.status === HTTP_FORBIDDEN) {
store.dispatch(publishNotification('error', 'Session timed out. Please login again.'))
} else {
store.dispatch(publishNotification('error', 'Cannot communicate with server.'))
}
this.setState({loggedIn: false})
dispatch(errorThrown(error))
}
},
render() {
if (this.state.loggedIn === null) {
return <div className="page-spinner"></div>
}
if (this.state.loggedIn === false) {
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/login" component={Login} />
<Redirect from="*" to="/login" />
</Router>
</Provider>
)
}
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={CreateSource} onEnter={this.redirectFromRoot} />
<Route path="/sources/new" component={CreateSource} />
<Route path="/sources/:sourceID" component={App}>
<Router history={history}>
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
<Route path="login" component={UserIsNotAuthenticated(Login)} />
<Route path="sources/new" component={UserIsAuthenticated(CreateSource)} />
<Route path="sources/:sourceID" component={UserIsAuthenticated(App)}>
<Route component={CheckSources}>
<Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourcePage} />
@ -140,7 +109,8 @@ const Root = React.createClass({
<Route path="hosts" component={HostsPage} />
<Route path="hosts/:hostID" component={HostPage} />
<Route path="kubernetes" component={KubernetesPage} />
<Route path="kapacitor-config" component={KapacitorPage} />
<Route path="kapacitors/new" component={KapacitorPage} />
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="alerts" component={AlertsApp} />
<Route path="dashboards" component={DashboardsPage} />

View File

@ -1,5 +1,5 @@
import uuid from 'node-uuid'
import {getKapacitor} from 'src/shared/apis'
import {getActiveKapacitor} from 'src/shared/apis'
import {publishNotification} from 'src/shared/actions/notifications'
import {
getRules,
@ -10,7 +10,7 @@ import {
export function fetchRule(source, ruleID) {
return (dispatch) => {
getKapacitor(source).then((kapacitor) => {
getActiveKapacitor(source).then((kapacitor) => {
getRule(kapacitor, ruleID).then(({data: rule}) => {
dispatch({
type: 'LOAD_RULE',

View File

@ -1,184 +0,0 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
import AlertaConfig from './AlertaConfig'
import HipChatConfig from './HipChatConfig'
import OpsGenieConfig from './OpsGenieConfig'
import PagerDutyConfig from './PagerDutyConfig'
import SensuConfig from './SensuConfig'
import SlackConfig from './SlackConfig'
import SMTPConfig from './SMTPConfig'
import TalkConfig from './TalkConfig'
import TelegramConfig from './TelegramConfig'
import VictorOpsConfig from './VictorOpsConfig'
const AlertOutputs = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
kapacitor: PropTypes.shape({
url: PropTypes.string.isRequired,
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}),
addFlashMessage: PropTypes.func.isRequired,
},
getInitialState() {
return {
selectedEndpoint: 'smtp',
configSections: null,
}
},
componentDidMount() {
this.refreshKapacitorConfig(this.props.kapacitor)
},
componentWillReceiveProps(nextProps) {
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
this.refreshKapacitorConfig(nextProps.kapacitor)
}
},
refreshKapacitorConfig(kapacitor) {
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
this.setState({configSections: sections})
}).catch(() => {
this.setState({configSections: null})
this.props.addFlashMessage({type: 'error', text: `There was an error getting the Kapacitor config`})
})
},
getSection(sections, section) {
return _.get(sections, [section, 'elements', '0'], null)
},
handleSaveConfig(section, properties) {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: `There was an error saving the kapacitor config`})
})
}
},
changeSelectedEndpoint(e) {
this.setState({
selectedEndpoint: e.target.value,
})
},
handleTest(section, properties) {
const propsToSend = this.sanitizeProperties(section, properties)
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: `There was an error testing the slack alert`})
})
},
sanitizeProperties(section, properties) {
const cleanProps = Object.assign({}, properties, {enabled: true})
const {redacted} = this.getSection(this.state.configSections, section)
if (redacted && redacted.length) {
redacted.forEach((badProp) => {
if (properties[badProp] === 'true') {
delete cleanProps[badProp]
}
})
}
return cleanProps
},
render() {
const {configSections, selectedEndpoint} = this.state
if (!configSections) { // could use this state to conditionally render spinner or error message
return null
}
return (
<div className="panel panel-minimal">
<div className="panel-body">
<h4 className="text-center no-user-select">Configure Alert Endpoints</h4>
<br/>
<div className="row">
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2">
<label htmlFor="alert-endpoint" className="sr-only">Alert Enpoint</label>
<select value={this.state.selectedEndpoint} className="form-control" id="source" onChange={this.changeSelectedEndpoint}>
<option value="alerta">Alerta</option>
<option value="hipchat">HipChat</option>
<option value="opsgenie">OpsGenie</option>
<option value="pagerduty">PagerDuty</option>
<option value="sensu">Sensu</option>
<option value="slack">Slack</option>
<option value="smtp">SMTP</option>
<option value="talk">Talk</option>
<option value="telegram">Telegram</option>
<option value="victorops">VictorOps</option>
</select>
</div>
</div>
<div className="row">
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
<hr/>
</div>
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
{this.renderAlertConfig(selectedEndpoint)}
</div>
</div>
</div>
</div>
)
},
renderAlertConfig(endpoint) {
const {configSections} = this.state
const save = (properties) => {
this.handleSaveConfig(endpoint, properties)
}
const test = (properties) => {
this.handleTest(endpoint, properties)
}
switch (endpoint) {
case 'alerta': {
return <AlertaConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'smtp': {
return <SMTPConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'slack': {
return <SlackConfig onSave={save} onTest={test} config={this.getSection(configSections, endpoint)} />
}
case 'victorops': {
return <VictorOpsConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'telegram': {
return <TelegramConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'opsgenie': {
return <OpsGenieConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'pagerduty': {
return <PagerDutyConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'hipchat': {
return <HipChatConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'sensu': {
return <SensuConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'talk': {
return <TalkConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
}
},
})
export default AlertOutputs

View File

@ -0,0 +1,189 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
import {
AlertaConfig,
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
SensuConfig,
SlackConfig,
SMTPConfig,
TalkConfig,
TelegramConfig,
VictorOpsConfig,
} from './config'
class AlertTabs extends Component {
constructor(props) {
super(props)
this.state = {
selectedEndpoint: 'smtp',
configSections: null,
}
this.refreshKapacitorConfig = ::this.refreshKapacitorConfig
this.getSection = ::this.getSection
this.handleSaveConfig = ::this.handleSaveConfig
this.handleTest = ::this.handleTest
this.sanitizeProperties = ::this.sanitizeProperties
}
componentDidMount() {
this.refreshKapacitorConfig(this.props.kapacitor)
}
componentWillReceiveProps(nextProps) {
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
this.refreshKapacitorConfig(nextProps.kapacitor)
}
}
refreshKapacitorConfig(kapacitor) {
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
this.setState({configSections: sections})
}).catch(() => {
this.setState({configSections: null})
this.props.addFlashMessage({type: 'error', text: 'There was an error getting the Kapacitor config'})
})
}
getSection(sections, section) {
return _.get(sections, [section, 'elements', '0'], null)
}
handleSaveConfig(section, properties) {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: 'There was an error saving the kapacitor config'})
})
}
}
handleTest(section, properties) {
const propsToSend = this.sanitizeProperties(section, properties)
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: 'There was an error testing the slack alert'})
})
}
sanitizeProperties(section, properties) {
const cleanProps = Object.assign({}, properties, {enabled: true})
const {redacted} = this.getSection(this.state.configSections, section)
if (redacted && redacted.length) {
redacted.forEach((badProp) => {
if (properties[badProp] === 'true') {
delete cleanProps[badProp]
}
})
}
return cleanProps
}
render() {
const {configSections} = this.state
if (!configSections) {
return null
}
const test = (properties) => {
this.handleTest('slack', properties)
}
const tabs = [
{
type: 'Alerta',
component: (<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')} />),
},
{
type: 'Slack',
component: (<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')} />),
},
{
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')} />),
},
{
type: 'Talk',
component: (<TalkConfig onSave={(p) => this.handleSaveConfig('talk', p)} config={this.getSection(configSections, 'talk')} />),
},
]
return (
<div>
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">Configure Alert Endpoints</h2>
</div>
</div>
<Tabs tabContentsClass="config-endpoint">
<TabList customClass="config-endpoint--tabs">
{
tabs.map((t, i) => (<Tab key={tabs[i].type}>{tabs[i].type}</Tab>))
}
</TabList>
<TabPanels customClass="config-endpoint--tab-contents">
{
tabs.map((t, i) => (<TabPanel key={tabs[i].type}>{t.component}</TabPanel>))
}
</TabPanels>
</Tabs>
</div>
)
}
}
const {
func,
shape,
string,
} = PropTypes
AlertTabs.propTypes = {
source: shape({
id: string.isRequired,
}).isRequired,
kapacitor: shape({
url: string.isRequired,
links: shape({
proxy: string.isRequired,
}).isRequired,
}),
addFlashMessage: func.isRequired,
}
export default AlertTabs

View File

@ -1,71 +0,0 @@
import React, {PropTypes} from 'react'
const AlertaConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
environment: PropTypes.string,
origin: PropTypes.string,
token: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
environment: this.environment.value,
origin: this.origin.value,
token: this.token.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {environment, origin, token, url} = this.props.config.options
return (
<div className="col-xs-12">
<h4 className="text-center no-user-select">Alerta Alert</h4>
<br/>
<form onSubmit={this.handleSaveAlert}>
<p className="no-user-select">
Have alerts sent to Alerta
</p>
<div className="form-group col-xs-12">
<label htmlFor="environment">Environment</label>
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="origin">Origin</label>
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">Token</label>
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
<span>Note: a value of <code>true</code> indicates the Alerta Token has been set</span>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">User</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default AlertaConfig

View File

@ -1,6 +1,4 @@
import React, {PropTypes} from 'react'
import classNames from 'classnames'
import _ from 'lodash'
import buildInfluxQLQuery from 'utils/influxql'
import DatabaseList from '../../data_explorer/components/DatabaseList'
@ -8,11 +6,6 @@ import MeasurementList from '../../data_explorer/components/MeasurementList'
import FieldList from '../../data_explorer/components/FieldList'
import TagList from '../../data_explorer/components/TagList'
const DB_TAB = 'databases'
const MEASUREMENTS_TAB = 'measurments'
const FIELDS_TAB = 'fields'
const TAGS_TAB = 'tags'
export const DataSection = React.createClass({
propTypes: {
source: PropTypes.shape({
@ -50,22 +43,12 @@ export const DataSection = React.createClass({
return {source: this.props.source}
},
getInitialState() {
return {
activeTab: DB_TAB,
}
},
handleChooseNamespace(namespace) {
this.props.actions.chooseNamespace(this.props.query.id, namespace)
this.setState({activeTab: MEASUREMENTS_TAB})
},
handleChooseMeasurement(measurement) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
this.setState({activeTab: FIELDS_TAB})
},
handleToggleField(field) {
@ -92,97 +75,50 @@ export const DataSection = React.createClass({
this.props.actions.groupByTag(this.props.query.id, tagKey)
},
handleClickTab(tab) {
this.setState({activeTab: tab})
},
render() {
const {query, timeRange: {lower}} = this.props
const statement = query.rawText || buildInfluxQLQuery({lower}, query) || `SELECT "fields" FROM "db"."rp"."measurement"`
const statement = query.rawText || buildInfluxQLQuery({lower}, query) || 'Build a query below'
return (
<div className="kapacitor-rule-section">
<div className="kapacitor-rule-section kapacitor-metric-selector">
<h3 className="rule-section-heading">Select a Time Series</h3>
<div className="rule-section-body">
<div className="qeditor kapacitor-metric-selector">
<div className="qeditor--query-preview">
<pre className={classNames("", {"rq-mode": query.rawText})}><code>{statement}</code></pre>
</div>
{this.renderEditor()}
</div>
<pre><code>{statement}</code></pre>
{this.renderQueryBuilder()}
</div>
</div>
)
},
renderEditor() {
const {activeTab} = this.state
renderQueryBuilder() {
const {query} = this.props
if (query.rawText) {
return (
<div className="generic-empty-state query-editor-empty-state">
<p className="margin-bottom-zero">
<span className="icon alert-triangle"></span>
&nbsp;Only editable in the <strong>Raw Query</strong> tab.
</p>
</div>
)
}
return (
<div className="kapacitor-tab-list">
<div className="qeditor--tabs">
<div onClick={_.wrap(DB_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === DB_TAB})}>Databases</div>
<div onClick={_.wrap(MEASUREMENTS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === MEASUREMENTS_TAB})}>Measurements</div>
<div onClick={_.wrap(FIELDS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === FIELDS_TAB})}>Fields</div>
<div onClick={_.wrap(TAGS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === TAGS_TAB})}>Tags</div>
</div>
{this.renderList()}
<div className="query-builder">
<DatabaseList
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
/>
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
isKapacitorRule={true}
/>
<TagList
query={query}
onChooseTag={this.handleChooseTag}
onGroupByTag={this.handleGroupByTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
/>
</div>
)
},
renderList() {
const {query} = this.props
switch (this.state.activeTab) {
case DB_TAB:
return (
<DatabaseList
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
)
case MEASUREMENTS_TAB:
return (
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
/>
)
case FIELDS_TAB:
return (
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
isKapacitorRule={true}
/>
)
case TAGS_TAB:
return (
<TagList
query={query}
onChooseTag={this.handleChooseTag}
onGroupByTag={this.handleGroupByTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
/>
)
default:
return <ul className="qeditor--list"></ul>
}
},
})
export default DataSection

View File

@ -1,100 +0,0 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const HipchatConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
room: this.room.value,
url: `https://${this.url.value}.hipchat.com/v2/room`,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url, room, token} = options
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
return (
<div>
<h4 className="text-center no-user-select">HipChat Alert</h4>
<br/>
<p className="no-user-select">Send alert messages to HipChat.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">Subdomain</label>
<input
className="form-control"
id="url"
type="text"
placeholder="your-subdomain"
ref={(r) => this.url = r}
defaultValue={subdomain && subdomain.length ? subdomain : ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="room">Room</label>
<input
className="form-control"
id="room"
type="text"
placeholder="your-hipchat-room"
ref={(r) => this.room = r}
defaultValue={room || ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={HIPCHAT_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-hipchat-token"
ref={(r) => this.token = r}
defaultValue={token || ''}
/>
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default HipchatConfig

View File

@ -1,31 +1,9 @@
import React, {PropTypes} from 'react'
import AlertOutputs from './AlertOutputs'
const {
func,
shape,
string,
bool,
} = PropTypes
const KapacitorForm = React.createClass({
propTypes: {
onSubmit: func.isRequired,
onInputChange: func.isRequired,
onReset: func.isRequired,
kapacitor: shape({
url: string.isRequired,
name: string.isRequired,
username: string,
password: string,
}).isRequired,
source: shape({}).isRequired,
addFlashMessage: func.isRequired,
exists: bool.isRequired,
},
import React, {Component, PropTypes} from 'react'
import AlertTabs from './AlertTabs'
class KapacitorForm extends Component {
render() {
const {onInputChange, onReset, kapacitor, source, onSubmit} = this.props
const {onInputChange, onReset, kapacitor, onSubmit} = this.props
const {url, name, username, password} = kapacitor
return (
@ -42,21 +20,15 @@ const KapacitorForm = React.createClass({
<div className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="col-md-3">
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">Connection Details</h2>
</div>
<div className="panel-body">
<p className="no-user-select">
Kapacitor is used as the monitoring and alerting agent.
This page will let you configure which Kapacitor to use and
set up alert end points like email, Slack, and others.
</p>
<hr/>
<h4 className="text-center no-user-select">Connect Kapacitor to Source</h4>
<h4 className="text-center">{source.url}</h4>
<br/>
<form onSubmit={onSubmit}>
<div>
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-2">
<div className="form-group">
<label htmlFor="url">Kapacitor URL</label>
<input
className="form-control"
@ -64,10 +36,11 @@ const KapacitorForm = React.createClass({
name="url"
placeholder={url}
value={url}
onChange={onInputChange}>
onChange={onInputChange}
spellCheck="false">
</input>
</div>
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-0">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
className="form-control"
@ -75,10 +48,11 @@ const KapacitorForm = React.createClass({
name="name"
placeholder={name}
value={name}
onChange={onInputChange}>
onChange={onInputChange}
spellCheck="false">
</input>
</div>
<div className="form-group col-xs-12 col-sm-4 col-sm-offset-2 col-md-4 col-md-offset-2">
<div className="form-group">
<label htmlFor="username">Username</label>
<input
className="form-control"
@ -86,10 +60,11 @@ const KapacitorForm = React.createClass({
name="username"
placeholder="username"
value={username}
onChange={onInputChange}>
onChange={onInputChange}
spellCheck="false">
</input>
</div>
<div className="form-group col-xs-12 col-sm-4 col-md-4">
<div className="form-group">
<label htmlFor="password">Password</label>
<input
className="form-control"
@ -99,21 +74,20 @@ const KapacitorForm = React.createClass({
placeholder="password"
value={password}
onChange={onInputChange}
spellCheck="false"
/>
</div>
</div>
<div className="form-group form-group-submit col-xs-12 text-center">
<button className="btn btn-info" type="button" onClick={onReset}>Reset to Default</button>
<button className="btn btn-success" type="submit">Connect Kapacitor</button>
<button className="btn btn-info" type="button" onClick={onReset}>Reset</button>
<button className="btn btn-success" type="submit">Connect</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="col-md-9">
{this.renderAlertOutputs()}
</div>
</div>
@ -121,26 +95,50 @@ const KapacitorForm = React.createClass({
</div>
</div>
)
},
}
// TODO: move these to another page. they dont belong on this page
renderAlertOutputs() {
const {exists, kapacitor, addFlashMessage, source} = this.props
if (exists) {
return <AlertOutputs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
return <AlertTabs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
}
return (
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">Configure Alert Endpoints</h2>
</div>
<div className="panel-body">
<h4 className="text-center">Configure Alert Endpoints</h4>
<br/>
<p className="text-center">Set your Kapacitor connection info to configure alerting endpoints.</p>
</div>
</div>
)
},
})
}
}
const {
func,
shape,
string,
bool,
} = PropTypes
KapacitorForm.propTypes = {
onSubmit: func.isRequired,
onInputChange: func.isRequired,
onReset: func.isRequired,
kapacitor: shape({
url: string.isRequired,
name: string.isRequired,
username: string,
password: string,
}).isRequired,
source: shape({}).isRequired,
addFlashMessage: func.isRequired,
exists: bool.isRequired,
}
export default KapacitorForm

View File

@ -87,9 +87,9 @@ export const KapacitorRule = React.createClass({
createRule(kapacitor, newRule).then(() => {
router.push(`/sources/${source.id}/alert-rules`)
addFlashMessage({type: 'success', text: `Rule successfully created`})
addFlashMessage({type: 'success', text: 'Rule successfully created'})
}).catch(() => {
addFlashMessage({type: 'error', text: `There was a problem creating the rule`})
addFlashMessage({type: 'error', text: 'There was a problem creating the rule'})
})
},
@ -101,9 +101,9 @@ export const KapacitorRule = React.createClass({
})
editRule(updatedRule).then(() => {
addFlashMessage({type: 'success', text: `Rule successfully updated!`})
addFlashMessage({type: 'success', text: 'Rule successfully updated!'})
}).catch(() => {
addFlashMessage({type: 'error', text: `There was a problem updating the rule`})
addFlashMessage({type: 'error', text: 'There was a problem updating the rule'})
})
},

View File

@ -40,7 +40,7 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => {
id={`kapacitor-enabled ${rule.id}`}
className="form-control-static"
type="checkbox"
defaultChecked={rule.status === "enabled"}
defaultChecked={rule.status === 'enabled'}
onClick={() => onChangeRuleStatus(rule)}
/>
<label htmlFor={`kapacitor-enabled ${rule.id}`}></label>

View File

@ -1,56 +0,0 @@
import React, {PropTypes} from 'react'
const PagerDutyConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'service-key': PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
serviceKey: this.serviceKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url} = options
const serviceKey = options['service-key']
return (
<div>
<h4 className="text-center no-user-select">PagerDuty Alert</h4>
<br/>
<p className="no-user-select">You can have alerts sent to PagerDuty by entering info below.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label>
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">PagerDuty URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default PagerDutyConfig

View File

@ -29,7 +29,7 @@ export const RuleGraph = React.createClass({
const autoRefreshMs = 30000
const queryText = buildInfluxQLQuery({lower}, query)
const queries = [{host: source.links.proxy, text: queryText}]
const kapacitorLineColors = ["#4ED8A0"]
const kapacitorLineColors = ['#4ED8A0']
if (!queryText) {
return (

View File

@ -22,11 +22,13 @@ const RuleMessageAlertConfig = ({
<p>{DEFAULT_ALERT_LABELS[alert]}</p>
<input
id="alert-input"
className="form-control size-486"
className="form-control size-486 form-control--green input-sm"
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>
)

View File

@ -1,74 +0,0 @@
import React, {PropTypes} from 'react'
const SMTPConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
host: PropTypes.string,
port: PropTypes.number,
username: PropTypes.string,
password: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
host: this.host.value,
port: this.port.value,
from: this.from.value,
username: this.username.value,
password: this.password.value,
}
this.props.onSave(properties)
},
render() {
const {host, port, from, username, password} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">SMTP Alert</h4>
<br/>
<p className="no-user-select">You can have alerts sent to an email address by setting up an SMTP endpoint.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-host">SMTP Host</label>
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-port">SMTP Port</label>
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="smtp-from">From Email</label>
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-user">User</label>
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-password">Password</label>
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SMTPConfig

View File

@ -1,53 +0,0 @@
import React, {PropTypes} from 'react'
const SensuConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
source: PropTypes.string.isRequired,
addr: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
source: this.source.value,
addr: this.addr.value,
}
this.props.onSave(properties)
},
render() {
const {source, addr} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Sensu Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to Sensu.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="source">Source</label>
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="address">Address</label>
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SensuConfig

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react'
const SlackConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
url: PropTypes.bool.isRequired,
channel: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
onTest: PropTypes.func.isRequired,
},
getInitialState() {
return {
testEnabled: !!this.props.config.options.url,
}
},
componentWillReceiveProps(nextProps) {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
channel: this.channel.value,
}
this.props.onSave(properties)
},
handleTest(e) {
e.preventDefault()
this.props.onTest({
url: this.url.value,
channel: this.channel.value,
})
},
render() {
const {url, channel} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Slack Alert</h4>
<br/>
<p className="no-user-select">Post alerts to a Slack channel.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="slack-channel">Slack Channel (optional)</label>
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 text-center">
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
<button className="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SlackConfig

View File

@ -1,61 +0,0 @@
import React, {PropTypes} from 'react'
const {
bool,
string,
shape,
func,
} = PropTypes
const TalkConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
author_name: this.author.value,
}
this.props.onSave(properties)
},
render() {
const {url, author_name: author} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Talk Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to Talk.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="author">Author Name</label>
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default TalkConfig

View File

@ -1,142 +0,0 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const TelegramConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
let parseMode
if (this.parseModeHTML.checked) {
parseMode = 'HTML'
}
if (this.parseModeMarkdown.checked) {
parseMode = 'Markdown'
}
const properties = {
'chat-id': this.chatID.value,
'disable-notification': this.disableNotification.checked,
'disable-web-page-preview': this.disableWebPagePreview.checked,
'parse-mode': parseMode,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {token} = options
const chatID = options['chat-id']
const disableNotification = options['disable-notification']
const disableWebPagePreview = options['disable-web-page-preview']
const parseMode = options['parse-mode']
return (
<div>
<h4 className="text-center no-user-select">Telegram Alert</h4>
<br/>
<p className="no-user-select">
Send alert messages to a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram bot</a>.
</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={TELEGRAM_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-telegram-token"
ref={(r) => this.token = r}
defaultValue={token || ''}>
</input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="chat-id">
Chat ID
<QuestionMarkTooltip
tipID="chat-id"
tipContent={TELEGRAM_CHAT_ID_TIP}
/>
</label>
<input
className="form-control"
id="chat-id"
type="text"
placeholder="your-telegram-chat-id"
ref={(r) => this.chatID = r}
defaultValue={chatID || ''}>
</input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="parseMode">Select the alert message format</label>
<div className="form-control-static">
<div className="radio">
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
<label htmlFor="parseModeMarkdown">Markdown</label>
</div>
<div className="radio">
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
<label htmlFor="parseModeHTML">HTML</label>
</div>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
<label htmlFor="disableWebPagePreview">
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
</label>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
<label htmlFor="disableNotification">
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
</label>
</div>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default TelegramConfig

View File

@ -101,9 +101,9 @@ const Threshold = React.createClass({
<span>{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}</span>
<p>is</p>
<Dropdown className="size-176 dropdown-kapacitor" items={operators} selected={operator} onChoose={this.handleDropdownChange} />
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange} />
<input className="form-control input-sm size-166 form-control--green" type="text" spellCheck="false" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange} />
{ (operator === 'inside range' || operator === 'outside range') &&
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueRangeInput = r} defaultValue={rangeValue} onKeyUp={this.handleInputChange} />
<input className="form-control input-sm size-166 form-control--green" type="text" spellCheck="false" ref={(r) => this.valueRangeInput = r} defaultValue={rangeValue} onKeyUp={this.handleInputChange} />
}
</div>
)
@ -153,6 +153,7 @@ const Relative = React.createClass({
onKeyUp={this.handleInputChange}
required={true}
type="text"
spellCheck="false"
/>
<p>{ change === CHANGES[1] ? '%' : '' }</p>
</div>

View File

@ -1,64 +0,0 @@
import React, {PropTypes} from 'react'
const VictorOpsConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'api-key': PropTypes.bool,
'routing-key': PropTypes.string,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
'api-key': this.apiKey.value,
'routing-key': this.routingKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const apiKey = options['api-key']
const routingKey = options['routing-key']
const {url} = options
return (
<div>
<h4 className="text-center no-user-select">VictorOps Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to VictorOps.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="routing-key">Routing Key</label>
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">VictorOps URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default VictorOpsConfig

View File

@ -0,0 +1,63 @@
import React, {PropTypes} from 'react'
const AlertaConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
environment: PropTypes.string,
origin: PropTypes.string,
token: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
environment: this.environment.value,
origin: this.origin.value,
token: this.token.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {environment, origin, token, url} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="environment">Environment</label>
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="origin">Origin</label>
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">Token</label>
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Alerta Token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">User</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default AlertaConfig

View File

@ -0,0 +1,95 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const HipchatConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
room: this.room.value,
url: `https://${this.url.value}.hipchat.com/v2/room`,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url, room, token} = options
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">Subdomain</label>
<input
className="form-control"
id="url"
type="text"
placeholder="your-subdomain"
ref={(r) => this.url = r}
defaultValue={subdomain && subdomain.length ? subdomain : ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="room">Room</label>
<input
className="form-control"
id="room"
type="text"
placeholder="your-hipchat-room"
ref={(r) => this.room = r}
defaultValue={room || ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={HIPCHAT_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-hipchat-token"
ref={(r) => this.token = r}
defaultValue={token || ''}
/>
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default HipchatConfig

View File

@ -64,25 +64,20 @@ const OpsGenieConfig = React.createClass({
const {currentTeams, currentRecipients} = this.state
return (
<div>
<h4 className="text-center no-user-select">OpsGenie Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to OpsGenie.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
</div>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
</div>
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})

View File

@ -0,0 +1,51 @@
import React, {PropTypes} from 'react'
const PagerDutyConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'service-key': PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
serviceKey: this.serviceKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url} = options
const serviceKey = options['service-key']
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label>
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">PagerDuty URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default PagerDutyConfig

View File

@ -0,0 +1,69 @@
import React, {PropTypes} from 'react'
const SMTPConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
host: PropTypes.string,
port: PropTypes.number,
username: PropTypes.string,
password: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
host: this.host.value,
port: this.port.value,
from: this.from.value,
username: this.username.value,
password: this.password.value,
}
this.props.onSave(properties)
},
render() {
const {host, port, from, username, password} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-host">SMTP Host</label>
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-port">SMTP Port</label>
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="smtp-from">From Email</label>
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-user">User</label>
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-password">Password</label>
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SMTPConfig

View File

@ -0,0 +1,48 @@
import React, {PropTypes} from 'react'
const SensuConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
source: PropTypes.string.isRequired,
addr: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
source: this.source.value,
addr: this.addr.value,
}
this.props.onSave(properties)
},
render() {
const {source, addr} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="source">Source</label>
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="address">Address</label>
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SensuConfig

View File

@ -0,0 +1,71 @@
import React, {PropTypes} from 'react'
const SlackConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
url: PropTypes.bool.isRequired,
channel: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
onTest: PropTypes.func.isRequired,
},
getInitialState() {
return {
testEnabled: !!this.props.config.options.url,
}
},
componentWillReceiveProps(nextProps) {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
channel: this.channel.value,
}
this.props.onSave(properties)
},
handleTest(e) {
e.preventDefault()
this.props.onTest({
url: this.url.value,
channel: this.channel.value,
})
},
render() {
const {url, channel} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="slack-channel">Slack Channel (optional)</label>
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 text-center">
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
<button className="btn btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SlackConfig

View File

@ -0,0 +1,56 @@
import React, {PropTypes} from 'react'
const {
bool,
string,
shape,
func,
} = PropTypes
const TalkConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
author_name: this.author.value,
}
this.props.onSave(properties)
},
render() {
const {url, author_name: author} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="author">Author Name</label>
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default TalkConfig

View File

@ -0,0 +1,138 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const TelegramConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
let parseMode
if (this.parseModeHTML.checked) {
parseMode = 'HTML'
}
if (this.parseModeMarkdown.checked) {
parseMode = 'Markdown'
}
const properties = {
'chat-id': this.chatID.value,
'disable-notification': this.disableNotification.checked,
'disable-web-page-preview': this.disableWebPagePreview.checked,
'parse-mode': parseMode,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {token} = options
const chatID = options['chat-id']
const disableNotification = options['disable-notification']
const disableWebPagePreview = options['disable-web-page-preview']
const parseMode = options['parse-mode']
return (
<form onSubmit={this.handleSaveAlert}>
<p className="no-user-select">
You need a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram Bot</a> to use this endpoint
</p>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={TELEGRAM_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-telegram-token"
ref={(r) => this.token = r}
defaultValue={token || ''}>
</input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="chat-id">
Chat ID
<QuestionMarkTooltip
tipID="chat-id"
tipContent={TELEGRAM_CHAT_ID_TIP}
/>
</label>
<input
className="form-control"
id="chat-id"
type="text"
placeholder="your-telegram-chat-id"
ref={(r) => this.chatID = r}
defaultValue={chatID || ''}>
</input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="parseMode">Select the alert message format</label>
<div className="form-control-static">
<div className="radio">
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
<label htmlFor="parseModeMarkdown">Markdown</label>
</div>
<div className="radio">
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
<label htmlFor="parseModeHTML">HTML</label>
</div>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
<label htmlFor="disableWebPagePreview">
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
</label>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
<label htmlFor="disableNotification">
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
</label>
</div>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default TelegramConfig

View File

@ -0,0 +1,59 @@
import React, {PropTypes} from 'react'
const VictorOpsConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'api-key': PropTypes.bool,
'routing-key': PropTypes.string,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
'api-key': this.apiKey.value,
'routing-key': this.routingKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const apiKey = options['api-key']
const routingKey = options['routing-key']
const {url} = options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="routing-key">Routing Key</label>
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">VictorOps URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default VictorOpsConfig

View File

@ -0,0 +1,23 @@
import AlertaConfig from './AlertaConfig'
import HipChatConfig from './HipChatConfig'
import OpsGenieConfig from './OpsGenieConfig'
import PagerDutyConfig from './PagerDutyConfig'
import SensuConfig from './SensuConfig'
import SlackConfig from './SlackConfig'
import SMTPConfig from './SMTPConfig'
import TalkConfig from './TalkConfig'
import TelegramConfig from './TelegramConfig'
import VictorOpsConfig from './VictorOpsConfig'
export {
AlertaConfig,
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
SensuConfig,
SlackConfig,
SMTPConfig,
TalkConfig,
TelegramConfig,
VictorOpsConfig,
}

View File

@ -29,14 +29,14 @@ export const ALERTS = ['alerta', 'hipchat', 'opsgenie', 'pagerduty', 'sensu', 's
export const DEFAULT_RULE_ID = 'DEFAULT_RULE_ID'
export const RULE_MESSAGE_TEMPLATES = {
id: {label: "{{.ID}}", text: "The ID of the alert"},
name: {label: "{{.Name}}", text: "Measurement name"},
taskName: {label: "{{.TaskName}}", text: "The name of the task"},
group: {label: "{{.Group}}", text: "Concatenation of all group-by tags of the form <code>&#91;key=value,&#93;+</code>. If no groupBy is performed equal to literal &quot;nil&quot;"},
tags: {label: "{{.Tags}}", text: "Map of tags. Use <code>&#123;&#123; index .Tags &quot;key&quot; &#125;&#125;</code> to get a specific tag value"},
level: {label: "{{.Level}}", text: "Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>"},
fields: {label: `{{ index .Fields "value" }}`, text: "Map of fields. Use <code>&#123;&#123; index .Fields &quot;key&quot; &#125;&#125;</code> to get a specific field value"},
time: {label: "{{.Time}}", text: "The time of the point that triggered the event"},
id: {label: '{{.ID}}', text: 'The ID of the alert'},
name: {label: '{{.Name}}', text: 'Measurement name'},
taskName: {label: '{{.TaskName}}', text: 'The name of the task'},
group: {label: '{{.Group}}', text: 'Concatenation of all group-by tags of the form <code>&#91;key=value,&#93;+</code>. If no groupBy is performed equal to literal &quot;nil&quot;'},
tags: {label: '{{.Tags}}', text: 'Map of tags. Use <code>&#123;&#123; index .Tags &quot;key&quot; &#125;&#125;</code> to get a specific tag value'},
level: {label: '{{.Level}}', text: 'Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>'},
fields: {label: '{{ index .Fields "value" }}', text: 'Map of fields. Use <code>&#123;&#123; index .Fields &quot;key&quot; &#125;&#125;</code> to get a specific field value'},
time: {label: '{{.Time}}', text: 'The time of the point that triggered the event'},
}
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec']
@ -51,8 +51,8 @@ export const DEFAULT_ALERT_LABELS = {
alerta: 'Paste Alerta TICKscript:',
}
export const DEFAULT_ALERT_PLACEHOLDERS = {
http: 'http://',
tcp: 'Address:',
http: 'Ex: http://example.com/api/alert',
tcp: 'Ex: exampleendpoint.com:5678',
exec: 'Ex: woogie boogie',
smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
slack: '#alerts',

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react'
import React, {Component, PropTypes} from 'react'
import {
getKapacitor,
createKapacitor,
@ -7,26 +7,13 @@ import {
} from 'shared/apis'
import KapacitorForm from '../components/KapacitorForm'
const defaultName = "My Kapacitor"
const kapacitorPort = "9092"
const defaultName = 'My Kapacitor'
const kapacitorPort = '9092'
const {
func,
shape,
string,
} = PropTypes
export const KapacitorPage = React.createClass({
propTypes: {
source: shape({
id: string.isRequired,
url: string.isRequired,
}),
addFlashMessage: func,
},
getInitialState() {
return {
class KapacitorPage extends Component {
constructor(props) {
super(props)
this.state = {
kapacitor: {
url: this._parseKapacitorURL(),
name: defaultName,
@ -35,39 +22,27 @@ export const KapacitorPage = React.createClass({
},
exists: false,
}
},
this.handleInputChange = ::this.handleInputChange
this.handleSubmit = ::this.handleSubmit
this.handleResetToDefaults = ::this.handleResetToDefaults
this._parseKapacitorURL = ::this._parseKapacitorURL
}
componentDidMount() {
const {source} = this.props
getKapacitor(source).then((kapacitor) => {
if (!kapacitor) {
return
}
const {source, params: {id}} = this.props
if (!id) {
return
}
getKapacitor(source, id).then((kapacitor) => {
this.setState({kapacitor, exists: true}, () => {
pingKapacitor(kapacitor).catch(() => {
this.props.addFlashMessage({type: 'error', text: 'Could not connect to Kapacitor. Check settings.'})
})
})
})
},
render() {
const {source, addFlashMessage} = this.props
const {kapacitor, exists} = this.state
return (
<KapacitorForm
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
onReset={this.handleResetToDefaults}
kapacitor={kapacitor}
source={source}
addFlashMessage={addFlashMessage}
exists={exists}
/>
)
},
}
handleInputChange(e) {
const {value, name} = e.target
@ -76,8 +51,7 @@ export const KapacitorPage = React.createClass({
const update = {[name]: value.trim()}
return {kapacitor: {...prevState.kapacitor, ...update}}
})
},
}
handleSubmit(e) {
e.preventDefault()
@ -99,7 +73,7 @@ export const KapacitorPage = React.createClass({
addFlashMessage({type: 'error', text: 'There was a problem creating the Kapacitor record'})
})
}
},
}
handleResetToDefaults(e) {
e.preventDefault()
@ -111,14 +85,48 @@ export const KapacitorPage = React.createClass({
}
this.setState({kapacitor: {...defaultState}})
},
}
_parseKapacitorURL() {
const parser = document.createElement('a')
parser.href = this.props.source.url
return `${parser.protocol}//${parser.hostname}:${kapacitorPort}`
},
})
}
render() {
const {source, addFlashMessage} = this.props
const {kapacitor, exists} = this.state
return (
<KapacitorForm
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
onReset={this.handleResetToDefaults}
kapacitor={kapacitor}
source={source}
addFlashMessage={addFlashMessage}
exists={exists}
/>
)
}
}
const {
func,
shape,
string,
} = PropTypes
KapacitorPage.propTypes = {
addFlashMessage: func,
params: shape({
id: string,
}).isRequired,
source: shape({
id: string.isRequired,
url: string.isRequired,
}),
}
export default KapacitorPage

View File

@ -1,11 +1,10 @@
import React, {PropTypes} from 'react'
import {withRouter} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
import * as kapacitorActionCreators from '../actions/view'
import * as queryActionCreators from '../../data_explorer/actions/view'
import {bindActionCreators} from 'redux'
import {getKapacitor, getKapacitorConfig} from 'shared/apis/index'
import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index'
import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'
import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
@ -53,7 +52,7 @@ export const KapacitorRulePage = React.createClass({
kapacitorActions.loadDefaultRule()
}
getKapacitor(source).then((kapacitor) => {
getActiveKapacitor(source).then((kapacitor) => {
this.setState({kapacitor})
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
const enabledAlerts = Object.keys(sections).filter((section) => {
@ -61,9 +60,9 @@ export const KapacitorRulePage = React.createClass({
})
this.setState({enabledAlerts})
}).catch(() => {
addFlashMessage({type: 'error', text: `There was a problem communicating with Kapacitor`})
addFlashMessage({type: 'error', text: 'There was a problem communicating with Kapacitor'})
}).catch(() => {
addFlashMessage({type: 'error', text: `We couldn't find a configured Kapacitor for this source`})
addFlashMessage({type: 'error', text: 'We couldn\'t find a configured Kapacitor for this source'})
})
})
},
@ -117,4 +116,4 @@ function mapDispatchToProps(dispatch) {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage))
export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage)

View File

@ -1,7 +1,7 @@
import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {getKapacitor} from 'src/shared/apis'
import {getActiveKapacitor} from 'src/shared/apis'
import * as kapacitorActionCreators from '../actions/view'
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
@ -18,7 +18,7 @@ class KapacitorRulesPage extends Component {
}
componentDidMount() {
getKapacitor(this.props.source).then((kapacitor) => {
getActiveKapacitor(this.props.source).then((kapacitor) => {
if (kapacitor) {
this.props.actions.fetchRules(kapacitor)
}

View File

@ -1,8 +1,28 @@
export function receiveAuth(auth) {
return {
type: 'AUTH_RECEIVED',
payload: {
auth,
},
}
}
export const authExpired = (auth) => ({
type: 'AUTH_EXPIRED',
payload: {
auth,
},
})
export const authRequested = () => ({
type: 'AUTH_REQUESTED',
})
export const authReceived = (auth) => ({
type: 'AUTH_RECEIVED',
payload: {
auth,
},
})
export const meRequested = () => ({
type: 'ME_REQUESTED',
})
export const meReceived = (me) => ({
type: 'ME_RECEIVED',
payload: {
me,
},
})

View File

@ -0,0 +1,5 @@
export const errorThrown = (error, altText) => ({
type: 'ERROR_THROWN',
error,
altText,
})

View File

@ -1,14 +0,0 @@
export function receiveMe(me) {
return {
type: 'ME_RECEIVED',
payload: {
me,
},
}
}
export function logout() {
return {
type: 'LOGOUT',
}
}

View File

@ -2,7 +2,7 @@ export function publishNotification(type, message) {
// this validator is purely for development purposes. It might make sense to move this to a middleware.
const validTypes = ['error', 'success', 'warning']
if (!validTypes.includes(type) || message === undefined) {
console.error("handleNotification must have a valid type and text") // eslint-disable-line no-console
console.error('handleNotification must have a valid type and text') // eslint-disable-line no-console
}
return {

View File

@ -1,4 +1,8 @@
import {deleteSource, getSources} from 'src/shared/apis'
import {deleteSource,
getSources,
getKapacitors as getKapacitorsAJAX,
updateKapacitor as updateKapacitorAJAX,
} from 'src/shared/apis'
import {publishNotification} from './notifications'
export const loadSources = (sources) => ({
@ -22,6 +26,21 @@ export const addSource = (source) => ({
},
})
export const fetchKapacitors = (source, kapacitors) => ({
type: 'LOAD_KAPACITORS',
payload: {
source,
kapacitors,
},
})
export const setActiveKapacitor = (kapacitor) => ({
type: 'SET_ACTIVE_KAPACITOR',
payload: {
kapacitor,
},
})
// Async action creators
export const removeAndLoadSources = (source) => async (dispatch) => {
@ -39,6 +58,22 @@ export const removeAndLoadSources = (source) => async (dispatch) => {
const {data: {sources: newSources}} = await getSources()
dispatch(loadSources(newSources))
} catch (err) {
dispatch(publishNotification("error", "Internal Server Error. Check API Logs"))
dispatch(publishNotification('error', 'Internal Server Error. Check API Logs'))
}
}
export const fetchKapacitorsAsync = (source) => async (dispatch) => {
try {
const {data} = await getKapacitorsAJAX(source)
dispatch(fetchKapacitors(source, data.kapacitors))
} catch (err) {
dispatch(publishNotification('error', `Internal Server Error. Could not retrieve kapacitors for source ${source.id}.`))
}
}
export const setActiveKapacitorAsync = (kapacitor) => async (dispatch) => {
// eagerly update the redux state
dispatch(setActiveKapacitor(kapacitor))
const kapacitorPost = {...kapacitor, active: true}
await updateKapacitorAJAX(kapacitorPost)
}

View File

@ -2,6 +2,8 @@ import {proxy} from 'utils/queryUrlGenerator'
import {noop} from 'shared/actions/app'
import _ from 'lodash'
import {errorThrown} from 'shared/actions/errors'
export const handleLoading = (query, editQueryStatus) => {
editQueryStatus(query.id, {loading: true})
}
@ -41,7 +43,7 @@ export const fetchTimeSeriesAsync = async ({source, db, rp, query, templates}, e
const {data} = await proxy({source, db, rp, query: query.text, templates})
return handleSuccess(data, query, editQueryStatus)
} catch (error) {
errorThrown(error)
handleError(error, query, editQueryStatus)
throw error
}
}

View File

@ -2,7 +2,7 @@ import AJAX from 'utils/ajax'
export function fetchLayouts() {
return AJAX({
url: `/chronograf/v1/layouts`,
url: '/chronograf/v1/layouts',
method: 'GET',
resource: 'layouts',
})
@ -58,15 +58,37 @@ export function pingKapacitor(kapacitor) {
})
}
export function getKapacitor(source) {
export function getKapacitor(source, kapacitorID) {
return AJAX({
url: `${source.links.kapacitors}/${kapacitorID}`,
method: 'GET',
}).then(({data}) => {
return data
})
}
export function getActiveKapacitor(source) {
return AJAX({
url: source.links.kapacitors,
method: 'GET',
}).then(({data}) => {
return data.kapacitors[0]
const activeKapacitor = data.kapacitors.find((k) => k.active)
return activeKapacitor || data.kapacitors[0]
})
}
export const getKapacitors = async (source) => {
try {
return await AJAX({
method: 'GET',
url: source.links.kapacitors,
})
} catch (error) {
console.error(error)
throw error
}
}
export function createKapacitor(source, {url, name = 'My Kapacitor', username, password}) {
return AJAX({
url: source.links.kapacitors,
@ -80,7 +102,7 @@ export function createKapacitor(source, {url, name = 'My Kapacitor', username, p
})
}
export function updateKapacitor({links, url, name = 'My Kapacitor', username, password}) {
export function updateKapacitor({links, url, name = 'My Kapacitor', username, password, active}) {
return AJAX({
url: links.self,
method: 'PATCH',
@ -89,6 +111,7 @@ export function updateKapacitor({links, url, name = 'My Kapacitor', username, pa
url,
username,
password,
active,
},
})
}

View File

@ -3,7 +3,7 @@ import _ from 'lodash'
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
export const showDatabases = async (source) => {
const query = `SHOW DATABASES`
const query = 'SHOW DATABASES'
return await proxy({source, query})
}
@ -52,7 +52,7 @@ export function showTagValues({source, database, retentionPolicy, measurement, t
export function showShards() {
return AJAX({
url: `/api/int/v1/show-shards`,
url: '/api/int/v1/show-shards',
})
}

View File

@ -13,30 +13,36 @@ const {
string,
} = PropTypes
const AutoRefresh = (ComposedComponent) => {
const AutoRefresh = ComposedComponent => {
const wrapper = React.createClass({
propTypes: {
children: element,
autoRefresh: number.isRequired,
templates: arrayOf(shape({
type: string.isRequired,
label: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string.isRequired,
rp: string,
influxql: string.isRequired,
}),
values: arrayOf(shape({
templates: arrayOf(
shape({
type: string.isRequired,
value: string.isRequired,
selected: bool,
})).isRequired,
})),
queries: arrayOf(shape({
host: oneOfType([string, arrayOf(string)]),
text: string,
}).isRequired).isRequired,
label: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string.isRequired,
rp: string,
influxql: string.isRequired,
}),
values: arrayOf(
shape({
type: string.isRequired,
value: string.isRequired,
selected: bool,
})
).isRequired,
})
),
queries: arrayOf(
shape({
host: oneOfType([string, arrayOf(string)]),
text: string,
}).isRequired
).isRequired,
editQueryStatus: func,
},
@ -51,30 +57,42 @@ const AutoRefresh = (ComposedComponent) => {
const {queries, autoRefresh} = this.props
this.executeQueries(queries)
if (autoRefresh) {
this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh)
this.intervalID = setInterval(
() => this.executeQueries(queries),
autoRefresh
)
}
},
componentWillReceiveProps(nextProps) {
const shouldRefetch = this.queryDifference(this.props.queries, nextProps.queries).length
const shouldRefetch = this.queryDifference(
this.props.queries,
nextProps.queries
).length
if (shouldRefetch) {
this.executeQueries(nextProps.queries)
}
if ((this.props.autoRefresh !== nextProps.autoRefresh) || shouldRefetch) {
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
clearInterval(this.intervalID)
if (nextProps.autoRefresh) {
this.intervalID = setInterval(() => this.executeQueries(nextProps.queries), nextProps.autoRefresh)
this.intervalID = setInterval(
() => this.executeQueries(nextProps.queries),
nextProps.autoRefresh
)
}
}
},
queryDifference(left, right) {
const leftStrs = left.map((q) => `${q.host}${q.text}`)
const rightStrs = right.map((q) => `${q.host}${q.text}`)
return _.difference(_.union(leftStrs, rightStrs), _.intersection(leftStrs, rightStrs))
const leftStrs = left.map(q => `${q.host}${q.text}`)
const rightStrs = right.map(q => `${q.host}${q.text}`)
return _.difference(
_.union(leftStrs, rightStrs),
_.intersection(leftStrs, rightStrs)
)
},
async executeQueries(queries) {
@ -87,13 +105,16 @@ const AutoRefresh = (ComposedComponent) => {
this.setState({isFetching: true})
const timeSeriesPromises = queries.map((query) => {
const timeSeriesPromises = queries.map(query => {
const {host, database, rp} = query
return fetchTimeSeriesAsync({source: host, db: database, rp, query, templates}, editQueryStatus)
return fetchTimeSeriesAsync(
{source: host, db: database, rp, query, templates},
editQueryStatus
)
})
Promise.all(timeSeriesPromises).then(timeSeries => {
const newSeries = timeSeries.map((response) => ({response}))
const newSeries = timeSeries.map(response => ({response}))
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
this.setState({
@ -116,16 +137,14 @@ const AutoRefresh = (ComposedComponent) => {
return this.renderFetching(timeSeries)
}
if (this._noResultsForQuery(timeSeries) || !this.state.lastQuerySuccessful) {
if (
this._noResultsForQuery(timeSeries) ||
!this.state.lastQuerySuccessful
) {
return this.renderNoResults()
}
return (
<ComposedComponent
{...this.props}
data={timeSeries}
/>
)
return <ComposedComponent {...this.props} data={timeSeries} />
},
/**
@ -161,8 +180,8 @@ const AutoRefresh = (ComposedComponent) => {
return true
}
return data.every((datum) => {
return datum.response.results.every((result) => {
return data.every(datum => {
return datum.response.results.every(result => {
return Object.keys(result).length === 0
})
})

View File

@ -47,13 +47,13 @@ const AutoRefreshDropdown = React.createClass({
const {milliseconds, inputValue} = this.findAutoRefreshItem(selected)
return (
<div className="dropdown time-range-dropdown">
<div className="dropdown dropdown-160">
<div className="btn btn-sm btn-info dropdown-toggle" onClick={() => self.toggleMenu()}>
<span className={classnames("icon", +milliseconds > 0 ? "refresh" : "pause")}></span>
<span className={classnames('icon', +milliseconds > 0 ? 'refresh' : 'pause')}></span>
<span className="selected-time-range">{inputValue}</span>
<span className="caret" />
</div>
<ul className={classnames("dropdown-menu", {show: isOpen})}>
<ul className={classnames('dropdown-menu', {show: isOpen})}>
<li className="dropdown-header">AutoRefresh Interval</li>
{autoRefreshItems.map((item) => {
return (

View File

@ -45,7 +45,7 @@ class CustomTimeRange extends Component {
const {isVisible, onToggle, timeRange: {upper, lower}} = this.props
return (
<div className={classNames("custom-time-range", {show: isVisible})} style={{display: 'flex'}}>
<div className={classNames('custom-time-range', {show: isVisible})} style={{display: 'flex'}}>
<button className="btn btn-sm btn-info custom-time-range--btn" onClick={onToggle}>
<span className="icon clock"></span>
{`${moment(lower).format('MMM Do HH:mm')}${moment(upper).format('MMM Do HH:mm')}`}

Some files were not shown because too many files have changed in this diff Show More