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] ## v1.2.0 [unreleased]
### Bug Fixes ### 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 ### Features
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to query config generation
### 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 ### Features
1. [#1292](https://github.com/influxdata/chronograf/pull/1292): Introduce Template Variable Manager 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. [#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. [#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. [#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. [#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. [#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 ### 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. [#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] ## 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. 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 query editor
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the raw query editor * Use Chronograf's query templates to easily explore your data
* Create visualizations and view query results in tabular format * Create visualizations and view query results in tabular format
### Dashboards ### 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) * [VictorOps](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#victorops)
* View all active alerts at a glance on the alerting dashboard * View all active alerts at a glance on the alerting dashboard
* Enable and disable existing alert rules with the check of a box * Enable and disable existing alert rules with the check of a box
* Configure multiple Kapacitor instances per InfluxDB source
### User and Query Management ### User and Query Management
@ -110,7 +111,7 @@ Change the default root path of the Chronograf server with the `--basepath` opti
## Versions ## 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. 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? Spotted a bug or have a feature request?

View File

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

View File

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

View File

@ -63,6 +63,7 @@ message Server {
string Password = 4; string Password = 4;
string URL = 5; // URL is the path to the server string URL = 5; // URL is the path to the server
int64 SrcID = 6; // SrcID is the ID of the data source int64 SrcID = 6; // SrcID is the ID of the data source
bool Active = 7; // is this the currently active server for the source
} }
message Layout { message Layout {

View File

@ -24,14 +24,9 @@ type ServersStore struct {
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) { func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
var srcs []chronograf.Server var srcs []chronograf.Server
if err := s.client.db.View(func(tx *bolt.Tx) error { if err := s.client.db.View(func(tx *bolt.Tx) error {
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error { var err error
var src chronograf.Server srcs, err = s.all(ctx, tx)
if err := internal.UnmarshalServer(v, &src); err != nil { if err != nil {
return err
}
srcs = append(srcs, src)
return nil
}); err != nil {
return err return err
} }
return nil return nil
@ -53,6 +48,10 @@ func (s *ServersStore) Add(ctx context.Context, src chronograf.Server) (chronogr
} }
src.ID = int(seq) 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 { if v, err := internal.MarshalServer(src); err != nil {
return err return err
} else if err := b.Put(itob(src.ID), v); err != nil { } 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 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 { if v, err := internal.MarshalServer(src); err != nil {
return err return err
} else if err := b.Put(itob(src.ID), v); err != nil { } 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 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", Username: "marty",
Password: "I❤ jennifer parker", Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local", URL: "toyota-hilux.lyon-estates.local",
Active: false,
}, },
chronograf.Server{ chronograf.Server{
Name: "HipToBeSquare", Name: "HipToBeSquare",
@ -34,6 +35,7 @@ func TestServerStore(t *testing.T) {
Username: "calvinklein", Username: "calvinklein",
Password: "chuck b3rry", Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local", 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") 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. // Delete an server.
if err := s.Delete(ctx, srcs[0]); err != nil { if err := s.Delete(ctx, srcs[0]); err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -324,6 +324,7 @@ type Server struct {
Username string // Username is the username to connect to the server Username string // Username is the username to connect to the server
Password string // Password is in CLEARTEXT Password string // Password is in CLEARTEXT
URL string // URL are the connections to the server 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` // 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 #### 1. Download and Install Chronograf
``` ```
wget https://dl.influxdata.com/chronograf/releases/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~beta8_amd64.deb sudo dpkg -i chronograf_1.2.0~beta9_amd64.deb
``` ```
#### 2. Start Chronograf #### 2. Start Chronograf

View File

@ -26,10 +26,18 @@ type cookie struct {
// NewCookieJWT creates an Authenticator that uses cookies for auth // NewCookieJWT creates an Authenticator that uses cookies for auth
func NewCookieJWT(secret string, lifespan time.Duration) Authenticator { 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{ return &cookie{
Name: DefaultCookieName, Name: DefaultCookieName,
Lifespan: lifespan, Lifespan: lifespan,
Inactivity: DefaultInactivityDuration, Inactivity: inactivity,
Now: DefaultNowTime, Now: DefaultNowTime,
Tokens: &JWT{ Tokens: &JWT{
Secret: secret, Secret: secret,
@ -44,6 +52,7 @@ func (c *cookie) Validate(ctx context.Context, r *http.Request) (Principal, erro
if err != nil { if err != nil {
return Principal{}, ErrAuthentication return Principal{}, ErrAuthentication
} }
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Lifespan) 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) // Only set a cookie to be persistent (endure beyond the browser session)
// if auth duration is greater than zero // if auth duration is greater than zero
if c.Lifespan > 0 || exp.Before(c.Now()) { if c.Lifespan > 0 {
cookie.Expires = exp cookie.Expires = exp
} }
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
} }
// Expire returns a cookie that will expire an existing cookie // Expire returns a cookie that will expire an existing cookie
func (c *cookie) Expire(w http.ResponseWriter) { func (c *cookie) Expire(w http.ResponseWriter) {
// to expire cookie set the time in the past // 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) { func TestNewCookieJWT(t *testing.T) {
auth := NewCookieJWT("secret", time.Second) auth := NewCookieJWT("secret", 2*time.Second)
if _, ok := auth.(*cookie); !ok { if cookie, ok := auth.(*cookie); !ok {
t.Errorf("NewCookieJWT() did not create cookie Authenticator") 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 // 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) { func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error) {
gojwt.TimeFunc = j.Now 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 // 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 // an invalid claim because server assumes that lifespan is the maximum possible
// duration // duration. However, a lifespan of zero means that the duration comparison
if exp.Sub(iat) > lifespan { // 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") 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", Desc: "Test jwt duration matches auth duration",
Secret: "secret", Secret: "secret",
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI", Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzMDAsImlhdCI6LTQ0Njc3NDQwMCwiaXNzIjoiaGlsbHZhbGxleSIsIm5iZiI6LTQ0Njc3NDQwMCwic3ViIjoibWFydHlAcGluaGVhZC5uZXQifQ.njEjstpuIDnghSR7VyPPB9QlvJ6Q5JpR3ZEZ_8vGYfA",
Duration: 500 * time.Hour, Duration: time.Second,
Principal: oauth2.Principal{ Principal: oauth2.Principal{
Subject: "/chronograf/v1/users/1", Subject: "marty@pinhead.net",
ExpiresAt: history, 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 { for _, test := range tests {
@ -97,6 +97,9 @@ func TestAuthenticate(t *testing.T) {
}, },
} }
principal, err := j.ValidPrincipal(context.Background(), test.Token, test.Duration) 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 err != nil {
if test.Err == nil { if test.Err == nil {
t.Errorf("Error in test %s authenticating with bad token: %v", test.Desc, err) 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 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 Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Active bool `json:"active"`
} }
func (p *postKapacitorRequest) Valid() error { 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) URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to kapacitor Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Active bool `json:"active"`
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor 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, Username: req.Username,
Password: req.Password, Password: req.Password,
URL: *req.URL, URL: *req.URL,
Active: req.Active,
} }
if srv, err = h.ServersStore.Add(ctx, srv); err != nil { if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
@ -102,6 +105,7 @@ func newKapacitor(srv chronograf.Server) kapacitor {
Username: srv.Username, Username: srv.Username,
Password: srv.Password, Password: srv.Password,
URL: srv.URL, URL: srv.URL,
Active: srv.Active,
Links: kapaLinks{ Links: kapaLinks{
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID), 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), 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 URL *string `json:"url,omitempty"` // URL for the kapacitor
Username *string `json:"username,omitempty"` // Username for kapacitor auth Username *string `json:"username,omitempty"` // Username for kapacitor auth
Password *string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
Active *bool `json:"active"`
} }
func (p *patchKapacitorRequest) Valid() error { func (p *patchKapacitorRequest) Valid() error {
@ -276,6 +281,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
if req.Username != nil { if req.Username != nil {
srv.Username = *req.Username srv.Username = *req.Username
} }
if req.Active != nil {
srv.Active = *req.Active
}
if err := h.ServersStore.Update(ctx, srv); err != nil { if err := h.ServersStore.Update(ctx, srv); err != nil {
msg := fmt.Sprintf("Error updating kapacitor ID %d", id) msg := fmt.Sprintf("Error updating kapacitor ID %d", id)

View File

@ -1154,7 +1154,7 @@
"required": true "required": true
} }
], ],
"summary": "Configured kapacitors", "summary": "Retrieve list of configured kapacitors",
"responses": { "responses": {
"200": { "200": {
"description": "An array of kapacitors", "description": "An array of kapacitors",
@ -1239,7 +1239,7 @@
} }
], ],
"summary": "Configured kapacitors", "summary": "Configured kapacitors",
"description": "These kapacitors are used for monitoring and alerting.", "description": "Retrieve information on a single kapacitor instance",
"responses": { "responses": {
"200": { "200": {
"description": "Kapacitor connection information", "description": "Kapacitor connection information",
@ -1334,7 +1334,8 @@
"required": true "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": { "responses": {
"204": { "204": {
"description": "kapacitor has been removed." "description": "kapacitor has been removed."
@ -1683,7 +1684,7 @@
"kapacitors", "kapacitors",
"proxy" "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": [ "parameters": [
{ {
"name": "id", "name": "id",
@ -2388,6 +2389,7 @@
"id": "4", "id": "4",
"name": "kapa", "name": "kapa",
"url": "http://localhost:9092", "url": "http://localhost:9092",
"active": false,
"links": { "links": {
"proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy", "proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy",
"self": "/chronograf/v1/sources/4/kapacitors/4", "self": "/chronograf/v1/sources/4/kapacitors/4",
@ -2417,6 +2419,10 @@
"format": "url", "format": "url",
"description": "URL for the kapacitor backend (e.g. http://localhost:9092)" "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": { "links": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

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

View File

@ -106,10 +106,12 @@
"react-grid-layout": "^0.13.9", "react-grid-layout": "^0.13.9",
"react-onclickoutside": "^5.2.0", "react-onclickoutside": "^5.2.0",
"react-redux": "^4.4.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-sparklines": "^1.4.2",
"react-tooltip": "^3.2.1", "react-tooltip": "^3.2.1",
"redux": "^3.3.1", "redux": "^3.3.1",
"redux-auth-wrapper": "^1.0.0",
"redux-thunk": "^1.0.3", "redux-thunk": "^1.0.3",
"rome": "^2.1.22", "rome": "^2.1.22",
"updeep": "^0.13.0" "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 React, {PropTypes} from 'react'
import {connect} from 'react-redux' 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 Notifications from 'shared/components/Notifications'
import {
publishNotification as publishNotificationAction, import {publishNotification} from 'src/shared/actions/notifications'
} from 'src/shared/actions/notifications'
const { const {
func, func,
node, node,
shape,
string,
} = PropTypes } = PropTypes
const App = React.createClass({ const App = React.createClass({
propTypes: { propTypes: {
children: node.isRequired, children: node.isRequired,
location: shape({
pathname: string,
}),
params: shape({
sourceID: string.isRequired,
}).isRequired,
notify: func.isRequired, notify: func.isRequired,
}, },
@ -32,16 +25,10 @@ const App = React.createClass({
}, },
render() { render() {
const {params: {sourceID}, location} = this.props
return ( return (
<div className="chronograf-root"> <div className="chronograf-root">
<SideNavContainer <SideNav />
sourceID={sourceID} <Notifications />
addFlashMessage={this.handleAddFlashMessage}
currentLocation={this.props.location.pathname}
/>
<Notifications location={location} />
{this.props.children && React.cloneElement(this.props.children, { {this.props.children && React.cloneElement(this.props.children, {
addFlashMessage: this.handleAddFlashMessage, addFlashMessage: this.handleAddFlashMessage,
})} })}
@ -50,6 +37,8 @@ const App = React.createClass({
}, },
}) })
export default connect(null, { const mapDispatchToProps = (dispatch) => ({
notify: publishNotificationAction, notify: bindActionCreators(publishNotification, dispatch),
})(App) })
export default connect(null, mapDispatchToProps)(App)

View File

@ -1,33 +1,33 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import {withRouter} from 'react-router' import {withRouter} from 'react-router'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {getSources} from 'src/shared/apis' import {bindActionCreators} from 'redux'
import {loadSources as loadSourcesAction} from 'src/shared/actions/sources'
import {showDatabases} from 'src/shared/apis/metaQuery' 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 // 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. // 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. // Routes that do require data nodes can be nested under this component.
const { const {arrayOf, func, node, shape, string} = PropTypes
arrayOf,
func,
node,
shape,
string,
} = PropTypes
const CheckSources = React.createClass({ const CheckSources = React.createClass({
propTypes: { propTypes: {
sources: arrayOf(shape({ sources: arrayOf(
links: shape({ shape({
proxy: string.isRequired, links: shape({
self: string.isRequired, proxy: string.isRequired,
kapacitors: string.isRequired, self: string.isRequired,
queries: string.isRequired, kapacitors: string.isRequired,
permissions: string.isRequired, queries: string.isRequired,
users: string.isRequired, permissions: string.isRequired,
databases: string.isRequired, users: string.isRequired,
}).isRequired, databases: string.isRequired,
})), }).isRequired,
})
),
addFlashMessage: func, addFlashMessage: func,
children: node, children: node,
params: shape({ params: shape({
@ -39,7 +39,8 @@ const CheckSources = React.createClass({
location: shape({ location: shape({
pathname: string.isRequired, pathname: string.isRequired,
}).isRequired, }).isRequired,
loadSourcesAction: func.isRequired, loadSources: func.isRequired,
errorThrown: func.isRequired,
}, },
childContextTypes: { childContextTypes: {
@ -58,7 +59,7 @@ const CheckSources = React.createClass({
getChildContext() { getChildContext() {
const {sources, params: {sourceID}} = this.props const {sources, params: {sourceID}} = this.props
return {source: sources.find((s) => s.id === sourceID)} return {source: sources.find(s => s.id === sourceID)}
}, },
getInitialState() { getInitialState() {
@ -67,56 +68,78 @@ const CheckSources = React.createClass({
} }
}, },
componentDidMount() { async componentWillMount() {
getSources().then(({data: {sources}}) => { const {loadSources, errorThrown} = this.props
this.props.loadSourcesAction(sources)
try {
const {data: {sources}} = await getSources()
loadSources(sources)
this.setState({isFetching: false}) this.setState({isFetching: false})
}).catch(() => { } catch (error) {
this.props.addFlashMessage({type: 'error', text: "Unable to connect to Chronograf server"}) errorThrown(error, 'Unable to connect to Chronograf server')
this.setState({isFetching: false}) this.setState({isFetching: false})
}) }
}, },
componentWillUpdate(nextProps, nextState) { async componentWillUpdate(nextProps, nextState) {
const {router, location, params, addFlashMessage, sources} = nextProps const {router, location, params, errorThrown, sources} = nextProps
const {isFetching} = nextState const {isFetching} = nextState
const source = sources.find((s) => s.id === params.sourceID) const source = sources.find(s => s.id === params.sourceID)
const defaultSource = sources.find((s) => s.default === true) const defaultSource = sources.find(s => s.default === true)
if (!isFetching && !source) { if (!isFetching && !source) {
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
const restString = rest === null ? 'hosts' : rest[1]
if (defaultSource) { if (defaultSource) {
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/) return router.push(`/sources/${defaultSource.id}/${restString}`)
return router.push(`/sources/${defaultSource.id}/${rest[1]}`) } else if (sources[0]) {
return router.push(`/sources/${sources[0].id}/${restString}`)
} }
return router.push(`/sources/new?redirectPath=${location.pathname}`) 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. // Do simple query to proxy to see if the source is up.
showDatabases(source.links.proxy).catch(() => { try {
addFlashMessage({type: 'error', text: `Unable to connect to source`}) await showDatabases(source.links.proxy)
}) } catch (error) {
errorThrown(error, 'Unable to connect to source')
}
} }
}, },
render() { render() {
const {params, sources} = this.props const {params, sources} = this.props
const {isFetching} = this.state 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) { if (isFetching || !source) {
return <div className="page-spinner" /> return <div className="page-spinner" />
} }
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, { return (
source, this.props.children &&
})) React.cloneElement(
this.props.children,
Object.assign({}, this.props, {
source,
})
)
)
}, },
}) })
function mapStateToProps(state) { const mapStateToProps = ({sources}) => ({
return { sources,
sources: state.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, killQuery as killQueryProxy,
} from 'shared/apis/metaQuery' } from 'shared/apis/metaQuery'
import {publishNotification} from 'shared/actions/notifications'
import {publishAutoDismissingNotification} from 'shared/dispatchers' import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {errorThrown} from 'shared/actions/errors'
import {REVERT_STATE_DELAY} from 'shared/constants' import {REVERT_STATE_DELAY} from 'shared/constants'
@ -221,23 +221,39 @@ export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
// async actions // async actions
export const loadUsersAsync = (url) => async (dispatch) => { export const loadUsersAsync = (url) => async (dispatch) => {
const {data} = await getUsersAJAX(url) try {
dispatch(loadUsers(data)) const {data} = await getUsersAJAX(url)
dispatch(loadUsers(data))
} catch (error) {
dispatch(errorThrown(error))
}
} }
export const loadRolesAsync = (url) => async (dispatch) => { export const loadRolesAsync = (url) => async (dispatch) => {
const {data} = await getRolesAJAX(url) try {
dispatch(loadRoles(data)) const {data} = await getRolesAJAX(url)
dispatch(loadRoles(data))
} catch (error) {
dispatch(errorThrown(error))
}
} }
export const loadPermissionsAsync = (url) => async (dispatch) => { export const loadPermissionsAsync = (url) => async (dispatch) => {
const {data} = await getPermissionsAJAX(url) try {
dispatch(loadPermissions(data)) const {data} = await getPermissionsAJAX(url)
dispatch(loadPermissions(data))
} catch (error) {
dispatch(errorThrown(error))
}
} }
export const loadDBsAndRPsAsync = (url) => async (dispatch) => { export const loadDBsAndRPsAsync = (url) => async (dispatch) => {
const {data: {databases}} = await getDbsAndRpsAJAX(url) try {
dispatch(loadDatabases(databases)) const {data: {databases}} = await getDbsAndRpsAJAX(url)
dispatch(loadDatabases(databases))
} catch (error) {
dispatch(errorThrown(error))
}
} }
export const createUserAsync = (url, user) => async (dispatch) => { 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(publishAutoDismissingNotification('success', 'User created successfully'))
dispatch(syncUser(user, data)) dispatch(syncUser(user, data))
} catch (error) { } catch (error) {
dispatch(errorThrown(error, `Failed to create user: ${error.data.message}`))
// undo optimistic update // undo optimistic update
dispatch(publishNotification('error', `Failed to create user: ${error.data.message}`))
setTimeout(() => dispatch(deleteUser(user)), REVERT_STATE_DELAY) 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(publishAutoDismissingNotification('success', 'Role created successfully'))
dispatch(syncRole(role, data)) dispatch(syncRole(role, data))
} catch (error) { } catch (error) {
dispatch(errorThrown(error, `Failed to create role: ${error.data.message}`))
// undo optimistic update // undo optimistic update
dispatch(publishNotification('error', `Failed to create role: ${error.data.message}`))
setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY) setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY)
} }
} }
@ -270,8 +286,8 @@ export const createDatabaseAsync = (url, database) => async (dispatch) => {
dispatch(syncDatabase(database, data)) dispatch(syncDatabase(database, data))
dispatch(publishAutoDismissingNotification('success', 'Database created successfully')) dispatch(publishAutoDismissingNotification('success', 'Database created successfully'))
} catch (error) { } catch (error) {
// undo optimistic update dispatch(errorThrown(error))
dispatch(publishNotification('error', `Failed to create database: ${error.data.message}`)) // undo optimistic upda, `Failed to create database: ${error.data.message}`te
setTimeout(() => dispatch(removeDatabase(database)), REVERT_STATE_DELAY) 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(publishAutoDismissingNotification('success', 'Retention policy created successfully'))
dispatch(syncRetentionPolicy(database, retentionPolicy, data)) dispatch(syncRetentionPolicy(database, retentionPolicy, data))
} catch (error) { } catch (error) {
// undo optimistic update dispatch(errorThrown(error))
dispatch(publishNotification('error', `Failed to create retention policy: ${error.data.message}`)) // undo optimistic upda, `Failed to create retention policy: ${error.data.message}`te
setTimeout(() => dispatch(removeRetentionPolicy(database, retentionPolicy)), REVERT_STATE_DELAY) 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(publishAutoDismissingNotification('success', 'Retention policy updated successfully'))
dispatch(syncRetentionPolicy(database, retentionPolicy, data)) dispatch(syncRetentionPolicy(database, retentionPolicy, data))
} catch (error) { } 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 // optimistic update
dispatch(killQuery(queryID)) dispatch(killQuery(queryID))
dispatch(setQueryToKill(null)) dispatch(setQueryToKill(null))
try {
// kill query on server // kill query on server
killQueryProxy(source, queryID) await killQueryProxy(source, queryID)
} catch (error) {
dispatch(errorThrown(error))
// TODO: handle failed killQuery
}
} }
export const deleteRoleAsync = (role) => async (dispatch) => { export const deleteRoleAsync = (role) => async (dispatch) => {
@ -314,7 +334,7 @@ export const deleteRoleAsync = (role) => async (dispatch) => {
await deleteRoleAJAX(role.links.self) await deleteRoleAJAX(role.links.self)
dispatch(publishAutoDismissingNotification('success', 'Role deleted')) dispatch(publishAutoDismissingNotification('success', 'Role deleted'))
} catch (error) { } 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) await deleteUserAJAX(user.links.self)
dispatch(publishAutoDismissingNotification('success', 'User deleted')) dispatch(publishAutoDismissingNotification('success', 'User deleted'))
} catch (error) { } 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) await deleteDatabaseAJAX(database.links.self)
dispatch(publishAutoDismissingNotification('success', 'Database deleted')) dispatch(publishAutoDismissingNotification('success', 'Database deleted'))
} catch (error) { } 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) await deleteRetentionPolicyAJAX(retentionPolicy.links.self)
dispatch(publishAutoDismissingNotification('success', `Retention policy ${retentionPolicy.name} deleted`)) dispatch(publishAutoDismissingNotification('success', `Retention policy ${retentionPolicy.name} deleted`))
} catch (error) { } 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(publishAutoDismissingNotification('success', 'Role users updated'))
dispatch(syncRole(role, data)) dispatch(syncRole(role, data))
} catch (error) { } 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(publishAutoDismissingNotification('success', 'Role permissions updated'))
dispatch(syncRole(role, data)) dispatch(syncRole(role, data))
} catch (error) { } 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(publishAutoDismissingNotification('success', 'User permissions updated'))
dispatch(syncUser(user, data)) dispatch(syncUser(user, data))
} catch (error) { } 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(publishAutoDismissingNotification('success', 'User roles updated'))
dispatch(syncUser(user, data)) dispatch(syncUser(user, data))
} catch (error) { } 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(publishAutoDismissingNotification('success', 'User password updated'))
dispatch(syncUser(user, data)) dispatch(syncUser(user, data))
} catch (error) { } 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} /> <RoleEditingRow role={role} onEdit={onEdit} onSave={onSave} isNew={isNew} />
<td></td> <td></td>
<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} /> <ConfirmButtons item={role} onConfirm={onSave} onCancel={onCancel} />
</td> </td>
</tr> </tr>

View File

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

View File

@ -4,6 +4,6 @@ export function getAlerts(source, timeRange) {
return proxy({ return proxy({
source, source,
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc`, 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 */ /* global VERSION */
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
const {array} = PropTypes import Notifications from 'shared/components/Notifications'
const Login = ({auth}) => ( const Login = ({authData: {auth}}) => {
<div className="auth-page"> if (auth.isAuthLoading) {
<div className="auth-box"> return <div className="page-spinner"></div>
<div className="auth-logo"></div> }
<h1 className="auth-text-logo">Chronograf</h1>
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p> return (
{auth.map(({name, login, label}) => ( <div>
<a key={name} className="btn btn-primary" href={login}> <Notifications />
<span className={`icon ${name}`}></span> <div className="auth-page">
Login with {label} <div className="auth-box">
</a> <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> </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) => ({ const {
auth: state.auth, 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' 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, deleteDashboardCell as deleteDashboardCellAJAX,
} from 'src/dashboards/apis' } from 'src/dashboards/apis'
import {publishNotification} from 'shared/actions/notifications'
import {publishAutoDismissingNotification} from 'shared/dispatchers' 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' import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
@ -138,6 +137,7 @@ export const getDashboardsAsync = () => async dispatch => {
const {data: {dashboards}} = await getDashboardsAJAX() const {data: {dashboards}} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards)) dispatch(loadDashboards(dashboards))
} catch (error) { } catch (error) {
dispatch(errorThrown(error))
console.error(error) console.error(error)
throw error throw error
} }
@ -159,7 +159,7 @@ export const updateDashboardCell = (dashboard, cell) => async dispatch => {
dispatch(syncDashboardCell(dashboard, data)) dispatch(syncDashboardCell(dashboard, data))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
// dispatch(errorThrown(error)) dispatch(errorThrown(error))
} }
} }
@ -174,13 +174,10 @@ export const deleteDashboardAsync = dashboard => async dispatch => {
) )
) )
} catch (error) { } catch (error) {
dispatch(deleteDashboardFailed(dashboard))
dispatch( dispatch(
publishNotification( errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`)
'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)) dispatch(addDashboardCell(dashboard, data))
} catch (error) { } catch (error) {
dispatch(errorThrown(error))
console.error(error) console.error(error)
throw error throw error
} }
@ -202,7 +200,7 @@ export const deleteDashboardCellAsync = cell => async dispatch => {
await deleteDashboardCellAJAX(cell) await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(cell)) dispatch(deleteDashboardCell(cell))
} catch (error) { } catch (error) {
console.error(error) dispatch(errorThrown(error))
throw error throw error
} }
} }

View File

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

View File

@ -9,7 +9,7 @@ const OverlayControls = (props) => {
const {onCancel, onSave, selectedGraphType, onSelectGraphType} = props const {onCancel, onSave, selectedGraphType, onSelectGraphType} = props
return ( return (
<div className="overlay-controls"> <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"> <div className="overlay-controls--right">
<p>Visualization Type:</p> <p>Visualization Type:</p>
<ul className="toggle toggle-sm"> <ul className="toggle toggle-sm">

View File

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

View File

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

View File

@ -61,19 +61,19 @@ const DatabaseList = React.createClass({
return ( return (
<div className="query-builder--column"> <div className="query-builder--column">
<div className="query-builder--column-heading">Databases</div> <div className="query-builder--heading">Databases</div>
<ul className="qeditor--list"> <div className="query-builder--list">
{this.state.namespaces.map((namespace) => { {this.state.namespaces.map((namespace) => {
const {database, retentionPolicy} = namespace const {database, retentionPolicy} = namespace
const isActive = database === query.database && retentionPolicy === query.retentionPolicy const isActive = database === query.database && retentionPolicy === query.retentionPolicy
return ( 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} {database}.{retentionPolicy}
</li> </div>
) )
})} })}
</ul> </div>
</div> </div>
) )
}, },

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ const MeasurementList = React.createClass({
getInitialState() { getInitialState() {
return { return {
measurements: [], measurements: [],
filterText: "", filterText: '',
} }
}, },
@ -72,11 +72,15 @@ const MeasurementList = React.createClass({
render() { render() {
return ( return (
<div className="query-builder--column"> <div className="query-builder--column">
<div className="query-builder--column-heading">Measurements</div> <div className="query-builder--heading">
{this.props.query.database ? <div className="qeditor--list-header"> <span>Measurements</span>
<input className="qeditor--filter" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} /> {this.props.query.database ?
<span className="icon search"></span> <div className="query-builder--filter">
</div> : null } <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()} {this.renderList()}
</div> </div>
) )
@ -84,20 +88,24 @@ const MeasurementList = React.createClass({
renderList() { renderList() {
if (!this.props.query.database) { 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)) const measurements = this.state.measurements.filter((m) => m.match(this.state.filterText))
return ( return (
<ul className="qeditor--list"> <div className="query-builder--list">
{measurements.map((measurement) => { {measurements.map((measurement) => {
const isActive = measurement === this.props.query.measurement const isActive = measurement === this.props.query.measurement
return ( 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 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 QueryEditor from './QueryEditor'
import QueryTabItem from './QueryTabItem'
import buildInfluxQLQuery from 'utils/influxql' import buildInfluxQLQuery from 'utils/influxql'
const { const {
arrayOf,
func,
node,
number,
shape,
string, string,
shape,
func,
} = PropTypes } = PropTypes
const QueryBuilder = React.createClass({ const QueryBuilder = React.createClass({
@ -20,7 +20,9 @@ const QueryBuilder = React.createClass({
queries: string.isRequired, queries: string.isRequired,
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
queries: arrayOf(shape({})).isRequired, query: shape({
id: string,
}).isRequired,
timeRange: shape({ timeRange: shape({
upper: string, upper: string,
lower: string, lower: string,
@ -28,103 +30,91 @@ const QueryBuilder = React.createClass({
actions: shape({ actions: shape({
chooseNamespace: func.isRequired, chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired, chooseMeasurement: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired, chooseTag: func.isRequired,
groupByTag: func.isRequired, groupByTag: func.isRequired,
addQuery: func.isRequired,
toggleField: func.isRequired, toggleField: func.isRequired,
groupByTime: func.isRequired, groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired, toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired, editRawTextAsync: func.isRequired,
}).isRequired, }).isRequired,
height: string,
top: string,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
children: node,
}, },
handleAddQuery() { handleChooseNamespace(namespace) {
const newIndex = this.props.queries.length this.props.actions.chooseNamespace(this.props.query.id, namespace)
this.props.actions.addQuery()
this.props.setActiveQueryIndex(newIndex)
}, },
handleAddRawQuery() { handleChooseMeasurement(measurement) {
const newIndex = this.props.queries.length this.props.actions.chooseMeasurement(this.props.query.id, measurement)
this.props.actions.addQuery({rawText: ''})
this.props.setActiveQueryIndex(newIndex)
}, },
getActiveQuery() { handleToggleField(field) {
const {queries, activeQueryIndex} = this.props this.props.actions.toggleField(this.props.query.id, field)
const activeQuery = queries[activeQueryIndex] },
const defaultQuery = queries[0]
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() { render() {
const {height, top} = this.props const {query, timeRange} = this.props
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
return ( return (
<div className="query-builder" style={{height, top}}> <div className="query-maker--tab-contents">
{this.renderQueryTabList()} <QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
{this.renderQueryEditor()} {this.renderLists()}
</div> </div>
) )
}, },
renderQueryEditor() {
const {timeRange, actions, source} = this.props
const query = this.getActiveQuery()
if (!query) { renderLists() {
return ( const {query} = this.props
<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>
)
}
return ( return (
<QueryEditor <div className="query-builder">
source={source} <DatabaseList
timeRange={timeRange} query={query}
query={query} onChooseNamespace={this.handleChooseNamespace}
actions={actions} />
onAddQuery={this.handleAddQuery} <MeasurementList
/> query={query}
) onChooseMeasurement={this.handleChooseMeasurement}
}, />
<FieldList
renderQueryTabList() { query={query}
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
return ( applyFuncsToField={this.handleApplyFuncsToField}
<div className="query-builder--tabs"> />
<div className="query-builder--tabs-heading"> <TagList
<h1>Queries</h1> query={query}
<div className="panel--tab-new btn btn-sm btn-primary dropdown-toggle" onClick={this.handleAddQuery}> onChooseTag={this.handleChooseTag}
<span className="icon plus"></span> onGroupByTag={this.handleGroupByTag}
</div> onToggleTagAcceptance={this.handleToggleTagAcceptance}
</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> </div>
) )
}, },

View File

@ -1,122 +1,120 @@
import React, {PropTypes} from 'react' 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' const ENTER = 13
import MeasurementList from './MeasurementList' const ESCAPE = 27
import FieldList from './FieldList' const {bool, func, shape, string} = PropTypes
import TagList from './TagList'
import RawQueryEditor from './RawQueryEditor'
import buildInfluxQLQuery from 'utils/influxql'
const {
string,
shape,
func,
} = PropTypes
const QueryEditor = React.createClass({ const QueryEditor = React.createClass({
propTypes: { propTypes: {
source: shape({ query: string.isRequired,
links: shape({ onUpdate: func.isRequired,
queries: string.isRequired, config: shape({
}).isRequired, status: shape({
}).isRequired, error: string,
query: shape({ loading: bool,
id: string, success: string,
}).isRequired, warn: string,
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,
}).isRequired, }).isRequired,
}, },
handleChooseNamespace(namespace) { getInitialState() {
this.props.actions.chooseNamespace(this.props.query.id, namespace) return {
value: this.props.query,
}
}, },
handleChooseMeasurement(measurement) { componentWillReceiveProps(nextProps) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement) if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
}, },
handleToggleField(field) { handleKeyDown(e) {
this.props.actions.toggleField(this.props.query.id, field) if (e.keyCode === ENTER) {
e.preventDefault()
this.handleUpdate()
} else if (e.keyCode === ESCAPE) {
this.setState({value: this.state.value}, () => {
this.editor.blur()
})
}
}, },
handleGroupByTime(time) { handleChange() {
this.props.actions.groupByTime(this.props.query.id, time) this.setState({
value: this.editor.value,
})
}, },
handleApplyFuncsToField(fieldFunc) { handleUpdate() {
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc) this.props.onUpdate(this.state.value)
}, },
handleChooseTag(tag) { handleChooseTemplate(template) {
this.props.actions.chooseTag(this.props.query.id, tag) this.setState({value: template.query})
},
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() { render() {
const {query, timeRange} = this.props const {config: {status}} = this.props
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || '' const {value} = this.state
return ( return (
<div className="query-builder--tab-contents"> <div className="query-editor">
<div> <textarea
<RawQueryEditor query={q} config={query} onUpdate={this.handleEditRawText} /> className="query-editor--field"
{this.renderLists()} onChange={this.handleChange}
</div> 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> </div>
) )
}, },
renderStatus(status) {
if (!status) {
return <div className="query-editor--status" />
}
renderLists() { if (status.loading) {
const {query} = this.props return (
<div className="query-editor--status">
<LoadingDots />
</div>
)
}
return ( return (
<div className="query-builder--columns"> <div
<DatabaseList className={classNames('query-editor--status', {
query={query} 'query-editor--error': status.error,
onChooseNamespace={this.handleChooseNamespace} 'query-editor--success': status.success,
/> 'query-editor--warning': status.warn,
<MeasurementList })}
query={query} >
onChooseMeasurement={this.handleChooseMeasurement} <span
/> className={classNames('icon', {
<FieldList stop: status.error,
query={query} checkmark: status.success,
onToggleField={this.handleToggleField} 'alert-triangle': status.warn,
onGroupByTime={this.handleGroupByTime} })}
applyFuncsToField={this.handleApplyFuncsToField}
/>
<TagList
query={query}
onChooseTag={this.handleChooseTag}
onGroupByTag={this.handleGroupByTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
/> />
{status.error || status.warn || status.success}
</div> </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 React, {PropTypes} from 'react'
import classNames from 'classnames' import classNames from 'classnames'
const QueryTabItem = React.createClass({ const QueryMakerTab = React.createClass({
propTypes: { propTypes: {
isActive: PropTypes.bool, isActive: PropTypes.bool,
query: PropTypes.shape({ query: PropTypes.shape({
@ -24,12 +24,12 @@ const QueryTabItem = React.createClass({
render() { render() {
return ( return (
<div className={classNames('query-builder--tab', {active: this.props.isActive})} onClick={this.handleSelect}> <div className={classNames('query-maker--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
<span className="query-builder--tab-label">{this.props.queryTabText}</span> <label>{this.props.queryTabText}</label>
<span className="query-builder--tab-delete" onClick={this.handleDelete}></span> <span className="query-maker--delete" onClick={this.handleDelete}></span>
</div> </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 React, {PropTypes} from 'react'
import {Table, Column, Cell} from 'fixed-data-table'
import Dimensions from 'react-dimensions' import Dimensions from 'react-dimensions'
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {Table, Column, Cell} from 'fixed-data-table'
const { const {
arrayOf, arrayOf,
func, func,
@ -105,7 +108,6 @@ const ChronoTable = React.createClass({
isLoading: false, isLoading: false,
cellData: emptyCells, cellData: emptyCells,
}) })
console.error(error)
throw error throw error
} }
}, },

View File

@ -91,13 +91,16 @@ const TagList = React.createClass({
return ( return (
<div className="query-builder--column"> <div className="query-builder--column">
<div className="query-builder--column-heading">Tags</div> <div className="query-builder--heading">
{(!query.database || !query.measurement || !query.retentionPolicy) ? null : <div className="qeditor--list-header"> <span>Tags</span>
<div className="toggle toggle-sm"> {(!query.database || !query.measurement || !query.retentionPolicy) ? null :
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: query.areTagsAccepted})}>=</div> <div className={cx('flip-toggle', {flipped: query.areTagsAccepted})} onClick={this.handleAcceptReject}>
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: !query.areTagsAccepted})}>!=</div> <div className="flip-toggle--container">
</div> <div className="flip-toggle--front">!=</div>
</div>} <div className="flip-toggle--back">=</div>
</div>
</div>}
</div>
{this.renderList()} {this.renderList()}
</div> </div>
) )
@ -106,11 +109,15 @@ const TagList = React.createClass({
renderList() { renderList() {
const {database, measurement, retentionPolicy} = this.props.query const {database, measurement, retentionPolicy} = this.props.query
if (!database || !measurement || !retentionPolicy) { 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 ( return (
<ul className="qeditor--list"> <div className="query-builder--list">
{_.map(this.state.tags, (tagValues, tagKey) => { {_.map(this.state.tags, (tagValues, tagKey) => {
return ( return (
<TagListItem <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)) const filtered = tagValues.filter((v) => v.match(this.state.filterText))
return ( return (
<li> <div className="query-builder--sub-list">
<div className="tag-value-list__filter-container"> <div className="query-builder--filter">
<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} /> <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> <span className="icon search"></span>
</div> </div>
<ul className="tag-value-list"> {filtered.map((v) => {
{filtered.map((v) => { const cx = classNames('query-builder--list-item', {active: selectedTagValues.indexOf(v) > -1})
const cx = classNames('tag-value-list__item qeditor--list-item', {active: selectedTagValues.indexOf(v) > -1}) return (
return ( <div className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
<li className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}> <span>
<div className="tag-value-list__checkbox"></div> <div className="query-builder--checkbox"></div>
<div className="tag-value-list__item-label">{v}</div> {v}
</li> </span>
) </div>
})} )
</ul> })}
</li> </div>
) )
}, },
@ -83,23 +83,20 @@ const TagListItem = React.createClass({
render() { render() {
const {tagKey, tagValues} = this.props const {tagKey, tagValues} = this.props
const {isOpen} = this.state const {isOpen} = this.state
const itemClasses = classNames("qeditor--list-item tag-list__item", {open: isOpen}) const tagItemLabel = `${tagKey}${tagValues.length}`
return ( return (
<div> <div>
<li className={itemClasses} onClick={this.handleClickKey}> <div className={classNames('query-builder--list-item', {active: isOpen})} onClick={this.handleClickKey}>
<div className="tag-list__title"> <span>
<div className="tag-list__caret"> <div className="query-builder--caret icon caret-right"></div>
<div className="icon caret-right"></div> {tagItemLabel}
</div> </span>
{tagKey}
<span className="badge">{tagValues.length}</span>
</div>
<div <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} onClick={this.handleGroupBy}>Group By {tagKey}
</div> </div>
</li> </div>
{isOpen ? this.renderTagValues() : null} {isOpen ? this.renderTagValues() : null}
</div> </div>
) )

View File

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

View File

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

View File

@ -3,18 +3,9 @@ import buildInfluxQLQuery from 'utils/influxql'
import classNames from 'classnames' import classNames from 'classnames'
import VisHeader from 'src/data_explorer/components/VisHeader' import VisHeader from 'src/data_explorer/components/VisHeader'
import VisView from 'src/data_explorer/components/VisView' import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'src/shared/constants'
const GRAPH = 'graph' const {arrayOf, func, number, shape, string} = PropTypes
const TABLE = 'table'
const VIEWS = [GRAPH, TABLE]
const {
func,
arrayOf,
number,
shape,
string,
} = PropTypes
const Visualization = React.createClass({ const Visualization = React.createClass({
propTypes: { propTypes: {
@ -30,6 +21,7 @@ const Visualization = React.createClass({
height: string, height: string,
heightPixels: number, heightPixels: number,
editQueryStatus: func.isRequired, editQueryStatus: func.isRequired,
views: arrayOf(string).isRequired,
}, },
contextTypes: { contextTypes: {
@ -49,13 +41,25 @@ const Visualization = React.createClass({
} }
return { return {
view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH, view: typeof queryConfigs[activeQueryIndex].rawText === 'string'
? TABLE
: GRAPH,
}
},
getDefaultProps() {
return {
cellName: '',
} }
}, },
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const {queryConfigs, activeQueryIndex} = nextProps const {queryConfigs, activeQueryIndex} = nextProps
if (!queryConfigs.length || activeQueryIndex === null || activeQueryIndex === this.props.activeQueryIndex) { if (
!queryConfigs.length ||
activeQueryIndex === null ||
activeQueryIndex === this.props.activeQueryIndex
) {
return return
} }
@ -71,8 +75,10 @@ const Visualization = React.createClass({
render() { render() {
const { const {
views,
height, height,
cellType, cellType,
cellName,
timeRange, timeRange,
autoRefresh, autoRefresh,
heightPixels, heightPixels,
@ -83,18 +89,28 @@ const Visualization = React.createClass({
const {source: {links: {proxy}}} = this.context const {source: {links: {proxy}}} = this.context
const {view} = this.state const {view} = this.state
const statements = queryConfigs.map((query) => { const statements = queryConfigs.map(query => {
const text = query.rawText || buildInfluxQLQuery(timeRange, query) const text = query.rawText || buildInfluxQLQuery(timeRange, query)
return {text, id: query.id} 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 {host: [proxy], text: s.text, id: s.id}
}) })
return ( return (
<div className="graph" style={{height}}> <div className="graph" style={{height}}>
<VisHeader views={VIEWS} view={view} onToggleView={this.handleToggleView} name={name || 'Graph'}/> <VisHeader
<div className={classNames({"graph-container": view === GRAPH, "table-container": view === TABLE})}> views={views}
view={view}
onToggleView={this.handleToggleView}
name={cellName}
/>
<div
className={classNames({
'graph-container': view === GRAPH,
'table-container': view === TABLE,
})}
>
<VisView <VisView
view={view} view={view}
queries={queries} 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: '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: 'Drop Continuous Query', query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"'},
{text: 'Show Users', query: 'SHOW USERS'}, {text: 'Show Users', query: 'SHOW USERS'},
{text: 'Create User', query: `CREATE USER "username" WITH PASSWORD 'password'`}, {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 Admin User', query: 'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES'},
{text: 'Drop User', query: 'DROP USER "username"'}, {text: 'Drop User', query: 'DROP USER "username"'},
{text: 'Show Stats', query: 'SHOW STATS'}, {text: 'Show Stats', query: 'SHOW STATS'},
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'}, {text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import _ from 'lodash'
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) { export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
return proxy({ return proxy({
source: proxyLink, 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, db: telegrafDB,
}).then((resp) => { }).then((resp) => {
const hosts = {} const hosts = {}
@ -88,6 +88,7 @@ export async function getAllHosts(proxyLink, telegrafDB) {
return hosts return hosts
} catch (error) { } catch (error) {
console.error(error) // eslint-disable-line no-console console.error(error) // eslint-disable-line no-console
throw error
} }
} }

View File

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

View File

@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import {render} from 'react-dom' import {render} from 'react-dom'
import {Provider} from 'react-redux' 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 {createHistory} from 'history'
import {syncHistoryWithStore} from 'react-router-redux'
import App from 'src/App' import App from 'src/App'
import AlertsApp from 'src/alerts' import AlertsApp from 'src/alerts'
import CheckSources from 'src/CheckSources' import CheckSources from 'src/CheckSources'
import {HostsPage, HostPage} from 'src/hosts' import {HostsPage, HostPage} from 'src/hosts'
import {KubernetesPage} from 'src/kubernetes' 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 {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor'
import DataExplorer from 'src/data_explorer' import DataExplorer from 'src/data_explorer'
import {DashboardsPage, DashboardPage} from 'src/dashboards' import {DashboardsPage, DashboardPage} from 'src/dashboards'
@ -17,18 +18,18 @@ import {CreateSource, SourcePage, ManageSources} from 'src/sources'
import {AdminPage} from 'src/admin' import {AdminPage} from 'src/admin'
import NotFound from 'src/shared/components/NotFound' import NotFound from 'src/shared/components/NotFound'
import configureStore from 'src/store/configureStore' 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 {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 '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') const rootNode = document.getElementById('react-root')
let browserHistory let browserHistory
@ -40,98 +41,66 @@ if (basepath) {
}) })
} else { } else {
browserHistory = useRouterHistory(createHistory)({ browserHistory = useRouterHistory(createHistory)({
basename: "", basename: '',
}) })
} }
const store = configureStore(loadLocalStorage(), browserHistory)
const {dispatch} = store
browserHistory.listen(() => { browserHistory.listen(() => {
store.dispatch(disablePresentationMode()) dispatch(disablePresentationMode())
}) })
window.addEventListener('keyup', (event) => { window.addEventListener('keyup', (event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
store.dispatch(disablePresentationMode()) dispatch(disablePresentationMode())
} }
}) })
const history = syncHistoryWithStore(browserHistory, store)
const Root = React.createClass({ const Root = React.createClass({
getInitialState() { componentWillMount() {
return {
loggedIn: null,
}
},
componentDidMount() {
this.checkAuth() this.checkAuth()
}, },
activeSource(sources) {
const defaultSource = sources.find((s) => s.default) async checkAuth() {
if (defaultSource && defaultSource.id) { dispatch(authRequested())
return defaultSource dispatch(meRequested())
try {
await this.startHeartbeat({shouldDispatchResponse: true})
} catch (error) {
dispatch(errorThrown(error))
} }
return sources[0]
}, },
redirectFromRoot(_, replace, callback) { async startHeartbeat({shouldDispatchResponse}) {
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}) {
try { try {
const {data: me, auth} = await getMe() const {data: me, auth} = await getMe()
if (shouldDispatchResponse) { if (shouldDispatchResponse) {
store.dispatch(receiveMe(me)) dispatch(authReceived(auth))
store.dispatch(receiveAuth(auth)) dispatch(meReceived(me))
this.setState({loggedIn: true})
} }
setTimeout(this.heartbeat.bind(null, {shouldDispatchResponse: false}), HEARTBEAT_INTERVAL) setTimeout(() => {
if (store.getState().auth.me !== null) {
this.startHeartbeat({shouldDispatchResponse: false})
}
}, HEARTBEAT_INTERVAL)
} catch (error) { } catch (error) {
if (error.auth) { dispatch(errorThrown(error))
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})
} }
}, },
render() { 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 ( return (
<Provider store={store}> <Provider store={store}>
<Router history={browserHistory}> <Router history={history}>
<Route path="/" component={CreateSource} onEnter={this.redirectFromRoot} /> <Route path="/" component={UserIsAuthenticated(CheckSources)} />
<Route path="/sources/new" component={CreateSource} /> <Route path="login" component={UserIsNotAuthenticated(Login)} />
<Route path="/sources/:sourceID" component={App}> <Route path="sources/new" component={UserIsAuthenticated(CreateSource)} />
<Route path="sources/:sourceID" component={UserIsAuthenticated(App)}>
<Route component={CheckSources}> <Route component={CheckSources}>
<Route path="manage-sources" component={ManageSources} /> <Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourcePage} /> <Route path="manage-sources/new" component={SourcePage} />
@ -140,7 +109,8 @@ const Root = React.createClass({
<Route path="hosts" component={HostsPage} /> <Route path="hosts" component={HostsPage} />
<Route path="hosts/:hostID" component={HostPage} /> <Route path="hosts/:hostID" component={HostPage} />
<Route path="kubernetes" component={KubernetesPage} /> <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="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="alerts" component={AlertsApp} /> <Route path="alerts" component={AlertsApp} />
<Route path="dashboards" component={DashboardsPage} /> <Route path="dashboards" component={DashboardsPage} />

View File

@ -1,5 +1,5 @@
import uuid from 'node-uuid' 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 {publishNotification} from 'src/shared/actions/notifications'
import { import {
getRules, getRules,
@ -10,7 +10,7 @@ import {
export function fetchRule(source, ruleID) { export function fetchRule(source, ruleID) {
return (dispatch) => { return (dispatch) => {
getKapacitor(source).then((kapacitor) => { getActiveKapacitor(source).then((kapacitor) => {
getRule(kapacitor, ruleID).then(({data: rule}) => { getRule(kapacitor, ruleID).then(({data: rule}) => {
dispatch({ dispatch({
type: 'LOAD_RULE', 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 React, {PropTypes} from 'react'
import classNames from 'classnames'
import _ from 'lodash'
import buildInfluxQLQuery from 'utils/influxql' import buildInfluxQLQuery from 'utils/influxql'
import DatabaseList from '../../data_explorer/components/DatabaseList' 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 FieldList from '../../data_explorer/components/FieldList'
import TagList from '../../data_explorer/components/TagList' 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({ export const DataSection = React.createClass({
propTypes: { propTypes: {
source: PropTypes.shape({ source: PropTypes.shape({
@ -50,22 +43,12 @@ export const DataSection = React.createClass({
return {source: this.props.source} return {source: this.props.source}
}, },
getInitialState() {
return {
activeTab: DB_TAB,
}
},
handleChooseNamespace(namespace) { handleChooseNamespace(namespace) {
this.props.actions.chooseNamespace(this.props.query.id, namespace) this.props.actions.chooseNamespace(this.props.query.id, namespace)
this.setState({activeTab: MEASUREMENTS_TAB})
}, },
handleChooseMeasurement(measurement) { handleChooseMeasurement(measurement) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement) this.props.actions.chooseMeasurement(this.props.query.id, measurement)
this.setState({activeTab: FIELDS_TAB})
}, },
handleToggleField(field) { handleToggleField(field) {
@ -92,97 +75,50 @@ export const DataSection = React.createClass({
this.props.actions.groupByTag(this.props.query.id, tagKey) this.props.actions.groupByTag(this.props.query.id, tagKey)
}, },
handleClickTab(tab) {
this.setState({activeTab: tab})
},
render() { render() {
const {query, timeRange: {lower}} = this.props 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 ( return (
<div className="kapacitor-rule-section"> <div className="kapacitor-rule-section kapacitor-metric-selector">
<h3 className="rule-section-heading">Select a Time Series</h3> <h3 className="rule-section-heading">Select a Time Series</h3>
<div className="rule-section-body"> <div className="rule-section-body">
<div className="qeditor kapacitor-metric-selector"> <pre><code>{statement}</code></pre>
<div className="qeditor--query-preview"> {this.renderQueryBuilder()}
<pre className={classNames("", {"rq-mode": query.rawText})}><code>{statement}</code></pre>
</div>
{this.renderEditor()}
</div>
</div> </div>
</div> </div>
) )
}, },
renderEditor() { renderQueryBuilder() {
const {activeTab} = this.state
const {query} = this.props 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 ( return (
<div className="kapacitor-tab-list"> <div className="query-builder">
<div className="qeditor--tabs"> <DatabaseList
<div onClick={_.wrap(DB_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === DB_TAB})}>Databases</div> query={query}
<div onClick={_.wrap(MEASUREMENTS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === MEASUREMENTS_TAB})}>Measurements</div> onChooseNamespace={this.handleChooseNamespace}
<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> <MeasurementList
</div> query={query}
{this.renderList()} 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> </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 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 React, {Component, PropTypes} from 'react'
import AlertOutputs from './AlertOutputs' import AlertTabs from './AlertTabs'
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,
},
class KapacitorForm extends Component {
render() { render() {
const {onInputChange, onReset, kapacitor, source, onSubmit} = this.props const {onInputChange, onReset, kapacitor, onSubmit} = this.props
const {url, name, username, password} = kapacitor const {url, name, username, password} = kapacitor
return ( return (
@ -42,21 +20,15 @@ const KapacitorForm = React.createClass({
<div className="page-contents"> <div className="page-contents">
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <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 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"> <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}> <form onSubmit={onSubmit}>
<div> <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> <label htmlFor="url">Kapacitor URL</label>
<input <input
className="form-control" className="form-control"
@ -64,10 +36,11 @@ const KapacitorForm = React.createClass({
name="url" name="url"
placeholder={url} placeholder={url}
value={url} value={url}
onChange={onInputChange}> onChange={onInputChange}
spellCheck="false">
</input> </input>
</div> </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> <label htmlFor="name">Name</label>
<input <input
className="form-control" className="form-control"
@ -75,10 +48,11 @@ const KapacitorForm = React.createClass({
name="name" name="name"
placeholder={name} placeholder={name}
value={name} value={name}
onChange={onInputChange}> onChange={onInputChange}
spellCheck="false">
</input> </input>
</div> </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> <label htmlFor="username">Username</label>
<input <input
className="form-control" className="form-control"
@ -86,10 +60,11 @@ const KapacitorForm = React.createClass({
name="username" name="username"
placeholder="username" placeholder="username"
value={username} value={username}
onChange={onInputChange}> onChange={onInputChange}
spellCheck="false">
</input> </input>
</div> </div>
<div className="form-group col-xs-12 col-sm-4 col-md-4"> <div className="form-group">
<label htmlFor="password">Password</label> <label htmlFor="password">Password</label>
<input <input
className="form-control" className="form-control"
@ -99,21 +74,20 @@ const KapacitorForm = React.createClass({
placeholder="password" placeholder="password"
value={password} value={password}
onChange={onInputChange} onChange={onInputChange}
spellCheck="false"
/> />
</div> </div>
</div> </div>
<div className="form-group form-group-submit col-xs-12 text-center"> <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-info" type="button" onClick={onReset}>Reset</button>
<button className="btn btn-success" type="submit">Connect Kapacitor</button> <button className="btn btn-success" type="submit">Connect</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> <div className="col-md-9">
<div className="row">
<div className="col-md-8 col-md-offset-2">
{this.renderAlertOutputs()} {this.renderAlertOutputs()}
</div> </div>
</div> </div>
@ -121,26 +95,50 @@ const KapacitorForm = React.createClass({
</div> </div>
</div> </div>
) )
}, }
// TODO: move these to another page. they dont belong on this page // TODO: move these to another page. they dont belong on this page
renderAlertOutputs() { renderAlertOutputs() {
const {exists, kapacitor, addFlashMessage, source} = this.props const {exists, kapacitor, addFlashMessage, source} = this.props
if (exists) { if (exists) {
return <AlertOutputs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} /> return <AlertTabs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
} }
return ( return (
<div className="panel panel-minimal"> <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"> <div className="panel-body">
<h4 className="text-center">Configure Alert Endpoints</h4>
<br/> <br/>
<p className="text-center">Set your Kapacitor connection info to configure alerting endpoints.</p> <p className="text-center">Set your Kapacitor connection info to configure alerting endpoints.</p>
</div> </div>
</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 export default KapacitorForm

View File

@ -87,9 +87,9 @@ export const KapacitorRule = React.createClass({
createRule(kapacitor, newRule).then(() => { createRule(kapacitor, newRule).then(() => {
router.push(`/sources/${source.id}/alert-rules`) router.push(`/sources/${source.id}/alert-rules`)
addFlashMessage({type: 'success', text: `Rule successfully created`}) addFlashMessage({type: 'success', text: 'Rule successfully created'})
}).catch(() => { }).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(() => { editRule(updatedRule).then(() => {
addFlashMessage({type: 'success', text: `Rule successfully updated!`}) addFlashMessage({type: 'success', text: 'Rule successfully updated!'})
}).catch(() => { }).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}`} id={`kapacitor-enabled ${rule.id}`}
className="form-control-static" className="form-control-static"
type="checkbox" type="checkbox"
defaultChecked={rule.status === "enabled"} defaultChecked={rule.status === 'enabled'}
onClick={() => onChangeRuleStatus(rule)} onClick={() => onChangeRuleStatus(rule)}
/> />
<label htmlFor={`kapacitor-enabled ${rule.id}`}></label> <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 autoRefreshMs = 30000
const queryText = buildInfluxQLQuery({lower}, query) const queryText = buildInfluxQLQuery({lower}, query)
const queries = [{host: source.links.proxy, text: queryText}] const queries = [{host: source.links.proxy, text: queryText}]
const kapacitorLineColors = ["#4ED8A0"] const kapacitorLineColors = ['#4ED8A0']
if (!queryText) { if (!queryText) {
return ( return (

View File

@ -22,11 +22,13 @@ const RuleMessageAlertConfig = ({
<p>{DEFAULT_ALERT_LABELS[alert]}</p> <p>{DEFAULT_ALERT_LABELS[alert]}</p>
<input <input
id="alert-input" id="alert-input"
className="form-control size-486" className="form-control size-486 form-control--green input-sm"
type="text" type="text"
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]} placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)} onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)}
value={ALERT_NODES_ACCESSORS[alert](rule)} value={ALERT_NODES_ACCESSORS[alert](rule)}
autoComplete="off"
spellCheck="false"
/> />
</div> </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> <span>{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}</span>
<p>is</p> <p>is</p>
<Dropdown className="size-176 dropdown-kapacitor" items={operators} selected={operator} onChoose={this.handleDropdownChange} /> <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') && { (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> </div>
) )
@ -153,6 +153,7 @@ const Relative = React.createClass({
onKeyUp={this.handleInputChange} onKeyUp={this.handleInputChange}
required={true} required={true}
type="text" type="text"
spellCheck="false"
/> />
<p>{ change === CHANGES[1] ? '%' : '' }</p> <p>{ change === CHANGES[1] ? '%' : '' }</p>
</div> </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 const {currentTeams, currentRecipients} = this.state
return ( return (
<div> <form onSubmit={this.handleSaveAlert}>
<h4 className="text-center no-user-select">OpsGenie Alert</h4> <div className="form-group col-xs-12">
<br/> <label htmlFor="api-key">API Key</label>
<p className="no-user-select">Have alerts sent to OpsGenie.</p> <input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<form onSubmit={this.handleSaveAlert}> <label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
<div className="form-group col-xs-12"> </div>
<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="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} /> <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"> <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> <button className="btn btn-block btn-primary" type="submit">Save</button>
</div> </div>
</form> </form>
</div>
) )
}, },
}) })

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 DEFAULT_RULE_ID = 'DEFAULT_RULE_ID'
export const RULE_MESSAGE_TEMPLATES = { export const RULE_MESSAGE_TEMPLATES = {
id: {label: "{{.ID}}", text: "The ID of the alert"}, id: {label: '{{.ID}}', text: 'The ID of the alert'},
name: {label: "{{.Name}}", text: "Measurement name"}, name: {label: '{{.Name}}', text: 'Measurement name'},
taskName: {label: "{{.TaskName}}", text: "The name of the task"}, 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;"}, 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"}, 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>"}, 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"}, 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"}, time: {label: '{{.Time}}', text: 'The time of the point that triggered the event'},
} }
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec'] export const DEFAULT_ALERTS = ['http', 'tcp', 'exec']
@ -51,8 +51,8 @@ export const DEFAULT_ALERT_LABELS = {
alerta: 'Paste Alerta TICKscript:', alerta: 'Paste Alerta TICKscript:',
} }
export const DEFAULT_ALERT_PLACEHOLDERS = { export const DEFAULT_ALERT_PLACEHOLDERS = {
http: 'http://', http: 'Ex: http://example.com/api/alert',
tcp: 'Address:', tcp: 'Ex: exampleendpoint.com:5678',
exec: 'Ex: woogie boogie', exec: 'Ex: woogie boogie',
smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com', smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
slack: '#alerts', slack: '#alerts',

View File

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

View File

@ -1,11 +1,10 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import {withRouter} from 'react-router'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import _ from 'lodash' import _ from 'lodash'
import * as kapacitorActionCreators from '../actions/view' import * as kapacitorActionCreators from '../actions/view'
import * as queryActionCreators from '../../data_explorer/actions/view' import * as queryActionCreators from '../../data_explorer/actions/view'
import {bindActionCreators} from 'redux' 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 {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'
import KapacitorRule from 'src/kapacitor/components/KapacitorRule' import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
@ -53,7 +52,7 @@ export const KapacitorRulePage = React.createClass({
kapacitorActions.loadDefaultRule() kapacitorActions.loadDefaultRule()
} }
getKapacitor(source).then((kapacitor) => { getActiveKapacitor(source).then((kapacitor) => {
this.setState({kapacitor}) this.setState({kapacitor})
getKapacitorConfig(kapacitor).then(({data: {sections}}) => { getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
const enabledAlerts = Object.keys(sections).filter((section) => { const enabledAlerts = Object.keys(sections).filter((section) => {
@ -61,9 +60,9 @@ export const KapacitorRulePage = React.createClass({
}) })
this.setState({enabledAlerts}) this.setState({enabledAlerts})
}).catch(() => { }).catch(() => {
addFlashMessage({type: 'error', text: `There was a problem communicating with Kapacitor`}) addFlashMessage({type: 'error', text: 'There was a problem communicating with Kapacitor'})
}).catch(() => { }).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 React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {getKapacitor} from 'src/shared/apis' import {getActiveKapacitor} from 'src/shared/apis'
import * as kapacitorActionCreators from '../actions/view' import * as kapacitorActionCreators from '../actions/view'
import KapacitorRules from 'src/kapacitor/components/KapacitorRules' import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
@ -18,7 +18,7 @@ class KapacitorRulesPage extends Component {
} }
componentDidMount() { componentDidMount() {
getKapacitor(this.props.source).then((kapacitor) => { getActiveKapacitor(this.props.source).then((kapacitor) => {
if (kapacitor) { if (kapacitor) {
this.props.actions.fetchRules(kapacitor) this.props.actions.fetchRules(kapacitor)
} }

View File

@ -1,8 +1,28 @@
export function receiveAuth(auth) { export const authExpired = (auth) => ({
return { type: 'AUTH_EXPIRED',
type: 'AUTH_RECEIVED', payload: {
payload: { auth,
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. // this validator is purely for development purposes. It might make sense to move this to a middleware.
const validTypes = ['error', 'success', 'warning'] const validTypes = ['error', 'success', 'warning']
if (!validTypes.includes(type) || message === undefined) { 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 { 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' import {publishNotification} from './notifications'
export const loadSources = (sources) => ({ 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 // Async action creators
export const removeAndLoadSources = (source) => async (dispatch) => { export const removeAndLoadSources = (source) => async (dispatch) => {
@ -39,6 +58,22 @@ export const removeAndLoadSources = (source) => async (dispatch) => {
const {data: {sources: newSources}} = await getSources() const {data: {sources: newSources}} = await getSources()
dispatch(loadSources(newSources)) dispatch(loadSources(newSources))
} catch (err) { } 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 {noop} from 'shared/actions/app'
import _ from 'lodash' import _ from 'lodash'
import {errorThrown} from 'shared/actions/errors'
export const handleLoading = (query, editQueryStatus) => { export const handleLoading = (query, editQueryStatus) => {
editQueryStatus(query.id, {loading: true}) 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}) const {data} = await proxy({source, db, rp, query: query.text, templates})
return handleSuccess(data, query, editQueryStatus) return handleSuccess(data, query, editQueryStatus)
} catch (error) { } catch (error) {
errorThrown(error)
handleError(error, query, editQueryStatus) handleError(error, query, editQueryStatus)
throw error
} }
} }

View File

@ -2,7 +2,7 @@ import AJAX from 'utils/ajax'
export function fetchLayouts() { export function fetchLayouts() {
return AJAX({ return AJAX({
url: `/chronograf/v1/layouts`, url: '/chronograf/v1/layouts',
method: 'GET', method: 'GET',
resource: 'layouts', 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({ return AJAX({
url: source.links.kapacitors, url: source.links.kapacitors,
method: 'GET', method: 'GET',
}).then(({data}) => { }).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}) { export function createKapacitor(source, {url, name = 'My Kapacitor', username, password}) {
return AJAX({ return AJAX({
url: source.links.kapacitors, 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({ return AJAX({
url: links.self, url: links.self,
method: 'PATCH', method: 'PATCH',
@ -89,6 +111,7 @@ export function updateKapacitor({links, url, name = 'My Kapacitor', username, pa
url, url,
username, username,
password, password,
active,
}, },
}) })
} }

View File

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

View File

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

View File

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

View File

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