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.jspull/1347/head
commit
0d1c416c98
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,21 +1,34 @@
|
|||
## v1.2.0 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
1. [#1257](https://github.com/influxdata/chronograf/issues/1257): Fix function selection in query builder
|
||||
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix env var name for Google client secret
|
||||
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to query config generation
|
||||
|
||||
### Features
|
||||
|
||||
### UI Improvements
|
||||
|
||||
## v1.2.0-beta9 [2017-04-21]
|
||||
|
||||
### Bug Fixes
|
||||
1. [#1257](https://github.com/influxdata/chronograf/issues/1257): Fix function selection in the query builder
|
||||
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix the environment variable name for Google client secret
|
||||
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to the explorer's query generation process
|
||||
1. [#1318](https://github.com/influxdata/chronograf/issues/1318): Fix JWT refresh for auth-durations of zero and less than five minutes
|
||||
1. [#1332](https://github.com/influxdata/chronograf/pull/1332): Remove table toggle from dashboard visualization
|
||||
|
||||
### Features
|
||||
1. [#1292](https://github.com/influxdata/chronograf/pull/1292): Introduce Template Variable Manager
|
||||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||
1. [#1265](https://github.com/influxdata/chronograf/pull/1265): Refactor the router to use auth and force /login route when auth expires
|
||||
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
||||
1. [#1316](https://github.com/influxdata/chronograf/pull/1316): Add templates API scoped within a dashboard
|
||||
1. [#1311](https://github.com/influxdata/chronograf/pull/1311): Display currently selected values in TVControlBar
|
||||
1. [#1315](https://github.com/influxdata/chronograf/pull/1315): Send selected TV values to proxy
|
||||
1. [#1302](https://github.com/influxdata/chronograf/pull/1302): Add support for multiple Kapacitors per InfluxDB source
|
||||
|
||||
### UI Improvements
|
||||
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add default display for empty dashboard
|
||||
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add a default display for empty dashboard
|
||||
1. [#1258](https://github.com/influxdata/chronograf/pull/1258): Display Kapacitor alert endpoint options as radio button group
|
||||
1. [#1321](https://github.com/influxdata/chronograf/pull/1321): Add yellow color to UI, Query Editor warnings are now appropriately colored
|
||||
|
||||
## v1.2.0-beta8 [2017-04-07]
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@ Currently, Chronograf offers dashboard templates for the following Telegraf inpu
|
|||
|
||||
Chronograf's graphing tool that allows you to dig in and create personalized visualizations of your data.
|
||||
|
||||
* Generate [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the query builder
|
||||
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the raw query editor
|
||||
* Generate and edit [InfluxQL](https://docs.influxdata.com/influxdb/latest/query_language/) statements with the query editor
|
||||
* Use Chronograf's query templates to easily explore your data
|
||||
* Create visualizations and view query results in tabular format
|
||||
|
||||
### Dashboards
|
||||
|
@ -91,6 +91,7 @@ A UI for [Kapacitor](https://github.com/influxdata/kapacitor) alert creation and
|
|||
* [VictorOps](https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#victorops)
|
||||
* View all active alerts at a glance on the alerting dashboard
|
||||
* Enable and disable existing alert rules with the check of a box
|
||||
* Configure multiple Kapacitor instances per InfluxDB source
|
||||
|
||||
### User and Query Management
|
||||
|
||||
|
@ -110,7 +111,7 @@ Change the default root path of the Chronograf server with the `--basepath` opti
|
|||
|
||||
## Versions
|
||||
|
||||
Chronograf v1.2.0-beta8 is a beta release.
|
||||
Chronograf v1.2.0-beta9 is a beta release.
|
||||
We will be iterating quickly based on user feedback and recommend using the [nightly builds](https://www.influxdata.com/downloads/) for the time being.
|
||||
|
||||
Spotted a bug or have a feature request?
|
||||
|
|
|
@ -54,6 +54,7 @@ func MarshalServer(s chronograf.Server) ([]byte, error) {
|
|||
Username: s.Username,
|
||||
Password: s.Password,
|
||||
URL: s.URL,
|
||||
Active: s.Active,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -70,6 +71,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
|
|||
s.Username = pb.Username
|
||||
s.Password = pb.Password
|
||||
s.URL = pb.URL
|
||||
s.Active = pb.Active
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,6 @@ It has these top-level messages:
|
|||
Source
|
||||
Dashboard
|
||||
DashboardCell
|
||||
Template
|
||||
TemplateValue
|
||||
TemplateQuery
|
||||
Server
|
||||
Layout
|
||||
Cell
|
||||
|
@ -59,10 +56,9 @@ func (*Source) ProtoMessage() {}
|
|||
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
|
||||
|
||||
type Dashboard struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
|
||||
Templates []*Template `protobuf:"bytes,4,rep,name=templates" json:"templates,omitempty"`
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Dashboard) Reset() { *m = Dashboard{} }
|
||||
|
@ -77,13 +73,6 @@ func (m *Dashboard) GetCells() []*DashboardCell {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Dashboard) GetTemplates() []*Template {
|
||||
if m != nil {
|
||||
return m.Templates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DashboardCell struct {
|
||||
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
|
||||
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
|
||||
|
@ -107,59 +96,6 @@ func (m *DashboardCell) GetQueries() []*Query {
|
|||
return nil
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
TempVar string `protobuf:"bytes,2,opt,name=temp_var,json=tempVar,proto3" json:"temp_var,omitempty"`
|
||||
Values []*TemplateValue `protobuf:"bytes,3,rep,name=values" json:"values,omitempty"`
|
||||
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Label string `protobuf:"bytes,5,opt,name=label,proto3" json:"label,omitempty"`
|
||||
Query *TemplateQuery `protobuf:"bytes,6,opt,name=query" json:"query,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Template) Reset() { *m = Template{} }
|
||||
func (m *Template) String() string { return proto.CompactTextString(m) }
|
||||
func (*Template) ProtoMessage() {}
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
||||
|
||||
func (m *Template) GetValues() []*TemplateValue {
|
||||
if m != nil {
|
||||
return m.Values
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Template) GetQuery() *TemplateQuery {
|
||||
if m != nil {
|
||||
return m.Query
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TemplateValue struct {
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
Selected bool `protobuf:"varint,3,opt,name=selected,proto3" json:"selected,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TemplateValue) Reset() { *m = TemplateValue{} }
|
||||
func (m *TemplateValue) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateValue) ProtoMessage() {}
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
|
||||
type TemplateQuery struct {
|
||||
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
|
||||
Db string `protobuf:"bytes,2,opt,name=db,proto3" json:"db,omitempty"`
|
||||
Rp string `protobuf:"bytes,3,opt,name=rp,proto3" json:"rp,omitempty"`
|
||||
Measurement string `protobuf:"bytes,4,opt,name=measurement,proto3" json:"measurement,omitempty"`
|
||||
TagKey string `protobuf:"bytes,5,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"`
|
||||
FieldKey string `protobuf:"bytes,6,opt,name=field_key,json=fieldKey,proto3" json:"field_key,omitempty"`
|
||||
}
|
||||
|
||||
func (m *TemplateQuery) Reset() { *m = TemplateQuery{} }
|
||||
func (m *TemplateQuery) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateQuery) ProtoMessage() {}
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
type Server struct {
|
||||
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
|
@ -167,12 +103,13 @@ type Server struct {
|
|||
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
|
||||
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
|
||||
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
|
||||
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Server) Reset() { *m = Server{} }
|
||||
func (m *Server) String() string { return proto.CompactTextString(m) }
|
||||
func (*Server) ProtoMessage() {}
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
|
||||
|
||||
type Layout struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -185,7 +122,7 @@ type Layout struct {
|
|||
func (m *Layout) Reset() { *m = Layout{} }
|
||||
func (m *Layout) String() string { return proto.CompactTextString(m) }
|
||||
func (*Layout) ProtoMessage() {}
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
|
||||
func (m *Layout) GetCells() []*Cell {
|
||||
if m != nil {
|
||||
|
@ -210,7 +147,7 @@ type Cell struct {
|
|||
func (m *Cell) Reset() { *m = Cell{} }
|
||||
func (m *Cell) String() string { return proto.CompactTextString(m) }
|
||||
func (*Cell) ProtoMessage() {}
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
func (m *Cell) GetQueries() []*Query {
|
||||
if m != nil {
|
||||
|
@ -232,7 +169,7 @@ type Query struct {
|
|||
func (m *Query) Reset() { *m = Query{} }
|
||||
func (m *Query) String() string { return proto.CompactTextString(m) }
|
||||
func (*Query) ProtoMessage() {}
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
|
||||
func (m *Query) GetRange() *Range {
|
||||
if m != nil {
|
||||
|
@ -249,7 +186,7 @@ type Range struct {
|
|||
func (m *Range) Reset() { *m = Range{} }
|
||||
func (m *Range) String() string { return proto.CompactTextString(m) }
|
||||
func (*Range) ProtoMessage() {}
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
|
||||
type AlertRule struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -261,7 +198,7 @@ type AlertRule struct {
|
|||
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
||||
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
|
||||
func (*AlertRule) ProtoMessage() {}
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
|
||||
type User struct {
|
||||
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
|
@ -271,15 +208,12 @@ type User struct {
|
|||
func (m *User) Reset() { *m = User{} }
|
||||
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||
func (*User) ProtoMessage() {}
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
||||
proto.RegisterType((*Template)(nil), "internal.Template")
|
||||
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
|
||||
proto.RegisterType((*TemplateQuery)(nil), "internal.TemplateQuery")
|
||||
proto.RegisterType((*Server)(nil), "internal.Server")
|
||||
proto.RegisterType((*Layout)(nil), "internal.Layout")
|
||||
proto.RegisterType((*Cell)(nil), "internal.Cell")
|
||||
|
@ -292,58 +226,47 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 848 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
|
||||
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xdd, 0x2d, 0x68, 0xb4, 0x62, 0x0d, 0xdc, 0x44, 0x16, 0x48,
|
||||
0x01, 0x89, 0x82, 0xd8, 0x27, 0x68, 0x6b, 0x09, 0x85, 0x76, 0x97, 0x32, 0x69, 0xcb, 0x15, 0x5a,
|
||||
0x4d, 0x92, 0x93, 0xd6, 0xda, 0x49, 0x6c, 0xc6, 0x76, 0xb3, 0x7e, 0x05, 0xc4, 0x05, 0x4f, 0x80,
|
||||
0xc4, 0x2d, 0x97, 0xbc, 0x00, 0x0f, 0xc1, 0x0b, 0xa1, 0x33, 0x33, 0xfe, 0x89, 0xb6, 0xa0, 0xbd,
|
||||
0xe2, 0x6e, 0xbe, 0x73, 0x26, 0xdf, 0x9c, 0x9f, 0xef, 0x73, 0xe0, 0x28, 0xdb, 0x55, 0xa8, 0x77,
|
||||
0x52, 0x1d, 0x17, 0x3a, 0xaf, 0x72, 0x1e, 0xb6, 0x38, 0xf9, 0x79, 0x04, 0xe3, 0x45, 0x5e, 0xeb,
|
||||
0x15, 0xf2, 0x23, 0x18, 0xcd, 0xd3, 0x98, 0x4d, 0xd9, 0xcc, 0x13, 0xa3, 0x79, 0xca, 0x39, 0xf8,
|
||||
0x2f, 0xe5, 0x16, 0xe3, 0xd1, 0x94, 0xcd, 0x22, 0x61, 0xce, 0x14, 0xbb, 0x6a, 0x0a, 0x8c, 0x3d,
|
||||
0x1b, 0xa3, 0x33, 0xff, 0x08, 0xc2, 0xeb, 0x92, 0xd8, 0xb6, 0x18, 0xfb, 0x26, 0xde, 0x61, 0xca,
|
||||
0x5d, 0xca, 0xb2, 0xdc, 0xe7, 0x7a, 0x1d, 0x07, 0x36, 0xd7, 0x62, 0xfe, 0x3e, 0x78, 0xd7, 0xe2,
|
||||
0x22, 0x1e, 0x9b, 0x30, 0x1d, 0x79, 0x0c, 0x93, 0x14, 0x37, 0xb2, 0x56, 0x55, 0x3c, 0x99, 0xb2,
|
||||
0x59, 0x28, 0x5a, 0x48, 0x3c, 0x57, 0xa8, 0xf0, 0x56, 0xcb, 0x4d, 0x1c, 0x5a, 0x9e, 0x16, 0xf3,
|
||||
0x63, 0xe0, 0xf3, 0x5d, 0x89, 0xab, 0x5a, 0xe3, 0xe2, 0x75, 0x56, 0xdc, 0xa0, 0xce, 0x36, 0x4d,
|
||||
0x1c, 0x19, 0x82, 0x07, 0x32, 0xf4, 0xca, 0x0b, 0xac, 0x24, 0xbd, 0x0d, 0x86, 0xaa, 0x85, 0xc9,
|
||||
0xaf, 0x0c, 0xa2, 0x54, 0x96, 0x77, 0xcb, 0x5c, 0xea, 0xf5, 0x3b, 0xcd, 0xe3, 0x0b, 0x08, 0x56,
|
||||
0xa8, 0x54, 0x19, 0x7b, 0x53, 0x6f, 0xf6, 0xe8, 0xeb, 0x67, 0xc7, 0xdd, 0xa0, 0x3b, 0x9e, 0x33,
|
||||
0x54, 0x4a, 0xd8, 0x5b, 0xfc, 0x2b, 0x88, 0x2a, 0xdc, 0x16, 0x4a, 0x56, 0x58, 0xc6, 0xbe, 0xf9,
|
||||
0x09, 0xef, 0x7f, 0x72, 0xe5, 0x52, 0xa2, 0xbf, 0x94, 0xfc, 0xc1, 0xe0, 0xc9, 0x01, 0x15, 0x7f,
|
||||
0x0c, 0xec, 0x8d, 0xa9, 0x2a, 0x10, 0xec, 0x0d, 0xa1, 0xc6, 0x54, 0x14, 0x08, 0xd6, 0x10, 0xda,
|
||||
0x9b, 0xdd, 0x04, 0x82, 0xed, 0x09, 0xdd, 0x99, 0x8d, 0x04, 0x82, 0xdd, 0xf1, 0xcf, 0x60, 0xf2,
|
||||
0x53, 0x8d, 0x3a, 0xc3, 0x32, 0x0e, 0xcc, 0xcb, 0xef, 0xf5, 0x2f, 0x7f, 0x5f, 0xa3, 0x6e, 0x44,
|
||||
0x9b, 0xa7, 0x4e, 0xcd, 0x36, 0xed, 0x6a, 0xcc, 0x99, 0x62, 0x15, 0x6d, 0x7e, 0x62, 0x63, 0x74,
|
||||
0x76, 0x13, 0xb2, 0xfb, 0x18, 0xcd, 0xd3, 0xe4, 0x2f, 0x46, 0x6b, 0xb2, 0xa5, 0x0f, 0xc6, 0x67,
|
||||
0x92, 0xfc, 0x43, 0x08, 0xa9, 0xad, 0x57, 0xf7, 0x52, 0xbb, 0x11, 0x4e, 0x08, 0xdf, 0x48, 0xcd,
|
||||
0xbf, 0x84, 0xf1, 0xbd, 0x54, 0x35, 0x3e, 0x30, 0xc6, 0x96, 0xee, 0x86, 0xf2, 0xc2, 0x5d, 0xeb,
|
||||
0x8a, 0xf1, 0x07, 0xc5, 0x3c, 0x85, 0x40, 0xc9, 0x25, 0x2a, 0xa7, 0x33, 0x0b, 0x68, 0x41, 0xd4,
|
||||
0x55, 0x63, 0x7a, 0x79, 0x90, 0xd9, 0xf6, 0x6e, 0x6f, 0x25, 0xd7, 0xf0, 0xe4, 0xe0, 0xc5, 0xee,
|
||||
0x25, 0x76, 0xf8, 0x92, 0xa9, 0xc3, 0xb5, 0x61, 0x01, 0x49, 0xb4, 0x44, 0x85, 0xab, 0x0a, 0xd7,
|
||||
0x66, 0x05, 0xa1, 0xe8, 0x70, 0xf2, 0x3b, 0xeb, 0x79, 0xcd, 0x7b, 0x24, 0xc2, 0x55, 0xbe, 0xdd,
|
||||
0xca, 0xdd, 0xda, 0x51, 0xb7, 0x90, 0xe6, 0xb6, 0x5e, 0x3a, 0xea, 0xd1, 0x7a, 0x49, 0x58, 0x17,
|
||||
0xce, 0x70, 0x23, 0x5d, 0xf0, 0x29, 0x3c, 0xda, 0xa2, 0x2c, 0x6b, 0x8d, 0x5b, 0xdc, 0x55, 0x6e,
|
||||
0x04, 0xc3, 0x10, 0x7f, 0x06, 0x93, 0x4a, 0xde, 0xbe, 0x7a, 0x8d, 0x8d, 0x9b, 0xc5, 0xb8, 0x92,
|
||||
0xb7, 0xe7, 0xd8, 0xf0, 0x8f, 0x21, 0xda, 0x64, 0xa8, 0xd6, 0x26, 0x65, 0x97, 0x1b, 0x9a, 0xc0,
|
||||
0x39, 0x36, 0xc9, 0x2f, 0x0c, 0xc6, 0x0b, 0xd4, 0xf7, 0xa8, 0xdf, 0x49, 0xf9, 0x43, 0xd7, 0x7b,
|
||||
0xff, 0xe1, 0x7a, 0xff, 0x61, 0xd7, 0x07, 0xbd, 0xeb, 0x9f, 0x42, 0xb0, 0xd0, 0xab, 0x79, 0x6a,
|
||||
0x2a, 0xf2, 0x84, 0x05, 0xc9, 0x6f, 0x0c, 0xc6, 0x17, 0xb2, 0xc9, 0xeb, 0xea, 0x2d, 0x25, 0x4d,
|
||||
0xe1, 0xd1, 0x49, 0x51, 0xa8, 0x6c, 0x25, 0xab, 0x2c, 0xdf, 0xb9, 0xaa, 0x86, 0x21, 0xba, 0xf1,
|
||||
0x62, 0x30, 0x23, 0x5b, 0xdf, 0x30, 0xc4, 0x3f, 0x81, 0xe0, 0xcc, 0x18, 0xd7, 0xba, 0xf0, 0xa8,
|
||||
0xd7, 0x85, 0xf5, 0xab, 0x49, 0x52, 0x23, 0x27, 0x75, 0x95, 0x6f, 0x54, 0xbe, 0x37, 0x15, 0x87,
|
||||
0xa2, 0xc3, 0xc9, 0xdf, 0x0c, 0xfc, 0xff, 0xcb, 0x90, 0x8f, 0x81, 0x65, 0x6e, 0x61, 0x2c, 0xeb,
|
||||
0xec, 0x39, 0x19, 0xd8, 0x33, 0x86, 0x49, 0xa3, 0xe5, 0xee, 0x16, 0xcb, 0x38, 0x9c, 0x7a, 0x33,
|
||||
0x4f, 0xb4, 0xd0, 0x64, 0x8c, 0x17, 0xca, 0x38, 0x9a, 0x7a, 0xa4, 0x34, 0x07, 0x3b, 0x6d, 0x43,
|
||||
0xaf, 0xed, 0xe4, 0x4f, 0x06, 0x41, 0xa7, 0xd0, 0xb3, 0x43, 0x85, 0x9e, 0xf5, 0x0a, 0x4d, 0x4f,
|
||||
0x5b, 0x85, 0xa6, 0xa7, 0x84, 0xc5, 0x65, 0xab, 0x50, 0x71, 0x49, 0x53, 0xfb, 0x46, 0xe7, 0x75,
|
||||
0x71, 0xda, 0xd8, 0xf1, 0x46, 0xa2, 0xc3, 0xfc, 0x03, 0x18, 0xff, 0x70, 0x87, 0xda, 0xf5, 0x1c,
|
||||
0x09, 0x87, 0x48, 0x04, 0x17, 0xc6, 0xbd, 0xb6, 0x4b, 0x0b, 0xf8, 0xa7, 0x10, 0x08, 0xea, 0xc2,
|
||||
0xb4, 0x7a, 0x30, 0x20, 0x13, 0x16, 0x36, 0x9b, 0x3c, 0x77, 0xd7, 0x88, 0xe5, 0xba, 0x28, 0x50,
|
||||
0x3b, 0xed, 0x5a, 0x60, 0xb8, 0xf3, 0x3d, 0xda, 0xcf, 0x8e, 0x27, 0x2c, 0x48, 0x7e, 0x84, 0xe8,
|
||||
0x44, 0xa1, 0xae, 0x44, 0xad, 0xde, 0xfe, 0x58, 0x71, 0xf0, 0xbf, 0x5d, 0x7c, 0xf7, 0xb2, 0x55,
|
||||
0x3c, 0x9d, 0x7b, 0x9d, 0x7a, 0x03, 0x9d, 0x52, 0x43, 0xe7, 0xb2, 0x90, 0xf3, 0xd4, 0x2c, 0xd6,
|
||||
0x13, 0x0e, 0x25, 0x9f, 0x83, 0x4f, 0x7e, 0x18, 0x30, 0xfb, 0xff, 0xe6, 0xa5, 0xe5, 0xd8, 0xfc,
|
||||
0x2b, 0x3f, 0xff, 0x27, 0x00, 0x00, 0xff, 0xff, 0xdd, 0x04, 0x69, 0xb6, 0xa7, 0x07, 0x00, 0x00,
|
||||
// 670 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xcd, 0x6e, 0xd3, 0x4a,
|
||||
0x14, 0xd6, 0xc4, 0x76, 0x7e, 0x4e, 0x7b, 0x7b, 0xaf, 0x46, 0x57, 0x30, 0x62, 0x15, 0x59, 0x20,
|
||||
0x05, 0x24, 0xba, 0xa0, 0x4f, 0x90, 0xd6, 0x12, 0x0a, 0xb4, 0xa5, 0x4c, 0x5a, 0x58, 0x81, 0x34,
|
||||
0x4d, 0x4f, 0x1a, 0x0b, 0xc7, 0x36, 0x63, 0xbb, 0xa9, 0x5f, 0x81, 0x87, 0x60, 0xc5, 0x8a, 0x25,
|
||||
0xaf, 0xc2, 0x0b, 0xa1, 0x33, 0x33, 0x76, 0x52, 0x28, 0xa8, 0x2b, 0x76, 0xe7, 0x3b, 0xc7, 0x39,
|
||||
0x3f, 0xdf, 0xf7, 0x4d, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
|
||||
0xef, 0x37, 0x38, 0xfc, 0xd4, 0x81, 0xee, 0x34, 0xab, 0xf4, 0x0c, 0xf9, 0x0e, 0x74, 0x26, 0x91,
|
||||
0x60, 0x43, 0x36, 0xf2, 0x64, 0x67, 0x12, 0x71, 0x0e, 0xfe, 0xb1, 0x5a, 0xa2, 0xe8, 0x0c, 0xd9,
|
||||
0x68, 0x20, 0x4d, 0x4c, 0xb9, 0xd3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0xf4, 0xcf,
|
||||
0x0a, 0xea, 0xb6, 0x44, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xa2, 0x8a, 0x62, 0x95, 0xe9, 0x0b,
|
||||
0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xc9, 0x43, 0xd1, 0x35, 0x69, 0x0a, 0xb9, 0x80,
|
||||
0x5e, 0x84, 0x73, 0x55, 0x25, 0xa5, 0xe8, 0x0d, 0xd9, 0xa8, 0x2f, 0x1b, 0x48, 0x7d, 0x4e, 0x31,
|
||||
0xc1, 0x4b, 0xad, 0xe6, 0xa2, 0x6f, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa4, 0x05, 0xce, 0x2a,
|
||||
0x8d, 0xd3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0x3c, 0xaf, 0xc5, 0xc0, 0x34, 0xb8, 0xa5, 0x42, 0x53,
|
||||
0x8e, 0xb0, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x10, 0xa9, 0x62, 0x71, 0x9e,
|
||||
0x29, 0x7d, 0x71, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc3, 0x24, 0x29, 0x84, 0x37, 0xf4, 0x46, 0x5b,
|
||||
0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x30, 0x49, 0xa4, 0xfd, 0x2a, 0xfc, 0xca, 0xe0,
|
||||
0x9f, 0x1b, 0x05, 0xbe, 0x0d, 0xec, 0xda, 0xcc, 0x08, 0x24, 0xbb, 0x26, 0x54, 0x9b, 0xfe, 0x81,
|
||||
0x64, 0x35, 0xa1, 0x95, 0x21, 0x3a, 0x90, 0x6c, 0x45, 0x68, 0x61, 0xe8, 0x0d, 0x24, 0x5b, 0xf0,
|
||||
0xc7, 0xd0, 0xfb, 0x58, 0xa1, 0x8e, 0xb1, 0x10, 0x81, 0x19, 0xfd, 0xef, 0x7a, 0xf4, 0xeb, 0x0a,
|
||||
0x75, 0x2d, 0x9b, 0x3a, 0xed, 0x6d, 0xa4, 0xb1, 0x3c, 0x9b, 0x98, 0x72, 0x25, 0xc9, 0xd8, 0xb3,
|
||||
0x39, 0x8a, 0xdd, 0xbd, 0x96, 0xdc, 0xce, 0x24, 0x0a, 0xbf, 0x30, 0xe8, 0x4e, 0x51, 0x5f, 0xa1,
|
||||
0xbe, 0x13, 0x15, 0x9b, 0x2e, 0xf0, 0xfe, 0xe0, 0x02, 0xff, 0x76, 0x17, 0x04, 0x6b, 0x17, 0xfc,
|
||||
0x0f, 0xc1, 0x54, 0xcf, 0x26, 0x91, 0xd9, 0xd8, 0x93, 0x16, 0xf0, 0x7b, 0xd0, 0x1d, 0xcf, 0xca,
|
||||
0xf8, 0x0a, 0x9d, 0x35, 0x1c, 0x0a, 0x3f, 0x33, 0xe8, 0x1e, 0xaa, 0x3a, 0xab, 0xca, 0x8d, 0x35,
|
||||
0xcd, 0x05, 0x7c, 0x08, 0x5b, 0xe3, 0x3c, 0x4f, 0xe2, 0x99, 0x2a, 0xe3, 0x2c, 0x75, 0xdb, 0x6e,
|
||||
0xa6, 0xe8, 0x8b, 0x23, 0x54, 0x45, 0xa5, 0x71, 0x89, 0x69, 0xe9, 0xf6, 0xde, 0x4c, 0xf1, 0x87,
|
||||
0x10, 0x1c, 0x18, 0x85, 0x7d, 0x43, 0xf3, 0xce, 0x9a, 0x66, 0x2b, 0xac, 0x29, 0xd2, 0x81, 0xe3,
|
||||
0xaa, 0xcc, 0xe6, 0x49, 0xb6, 0x32, 0x97, 0xf4, 0x65, 0x8b, 0xc3, 0xef, 0x0c, 0xfc, 0xbf, 0xa5,
|
||||
0xf5, 0x36, 0xb0, 0xd8, 0x09, 0xcd, 0xe2, 0x56, 0xf9, 0xde, 0x86, 0xf2, 0x02, 0x7a, 0xb5, 0x56,
|
||||
0xe9, 0x25, 0x16, 0xa2, 0x3f, 0xf4, 0x46, 0x9e, 0x6c, 0xa0, 0xa9, 0x24, 0xea, 0x1c, 0x93, 0x42,
|
||||
0x0c, 0x86, 0x1e, 0x3d, 0x0b, 0x07, 0x5b, 0xb7, 0xc0, 0xda, 0x2d, 0xe1, 0x37, 0x06, 0x81, 0x19,
|
||||
0x4e, 0xbf, 0x3b, 0xc8, 0x96, 0x4b, 0x95, 0x5e, 0x38, 0xea, 0x1b, 0x48, 0x7a, 0x44, 0xfb, 0x8e,
|
||||
0xf6, 0x4e, 0xb4, 0x4f, 0x58, 0x9e, 0x38, 0x92, 0x3b, 0xf2, 0x84, 0x58, 0x7b, 0xae, 0xb3, 0x2a,
|
||||
0xdf, 0xaf, 0x2d, 0xbd, 0x03, 0xd9, 0x62, 0x92, 0xfb, 0xed, 0x02, 0xb5, 0xbb, 0x79, 0x20, 0x1d,
|
||||
0x22, 0x73, 0x1c, 0xd2, 0x56, 0xee, 0x4a, 0x0b, 0xf8, 0x23, 0x08, 0x24, 0x5d, 0x61, 0x4e, 0xbd,
|
||||
0x41, 0x90, 0x49, 0x4b, 0x5b, 0x0d, 0xf7, 0xdc, 0x67, 0xd4, 0xe5, 0x2c, 0xcf, 0x51, 0x3b, 0x4f,
|
||||
0x5b, 0x60, 0x7a, 0x67, 0x2b, 0xd4, 0x66, 0x65, 0x4f, 0x5a, 0x10, 0xbe, 0x83, 0xc1, 0x38, 0x41,
|
||||
0x5d, 0xca, 0x2a, 0xc1, 0x5f, 0x2c, 0xc6, 0xc1, 0x7f, 0x31, 0x7d, 0x75, 0xdc, 0xbc, 0x04, 0x8a,
|
||||
0xd7, 0xfe, 0xf5, 0x7e, 0xf2, 0xef, 0x4b, 0x95, 0xab, 0x49, 0x64, 0x84, 0xf5, 0xa4, 0x43, 0xe1,
|
||||
0x13, 0xf0, 0xe9, 0x9d, 0x6c, 0x74, 0xf6, 0x7f, 0xf7, 0xc6, 0xce, 0xbb, 0xe6, 0xdf, 0x7b, 0xef,
|
||||
0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0x48, 0xbe, 0xb0, 0xc3, 0xcf, 0x05, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ message Server {
|
|||
string Password = 4;
|
||||
string URL = 5; // URL is the path to the server
|
||||
int64 SrcID = 6; // SrcID is the ID of the data source
|
||||
bool Active = 7; // is this the currently active server for the source
|
||||
}
|
||||
|
||||
message Layout {
|
||||
|
|
|
@ -24,14 +24,9 @@ type ServersStore struct {
|
|||
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
|
||||
var srcs []chronograf.Server
|
||||
if err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
|
||||
var src chronograf.Server
|
||||
if err := internal.UnmarshalServer(v, &src); err != nil {
|
||||
return err
|
||||
}
|
||||
srcs = append(srcs, src)
|
||||
return nil
|
||||
}); err != nil {
|
||||
var err error
|
||||
srcs, err = s.all(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -53,6 +48,10 @@ func (s *ServersStore) Add(ctx context.Context, src chronograf.Server) (chronogr
|
|||
}
|
||||
src.ID = int(seq)
|
||||
|
||||
// make the newly added source "active"
|
||||
s.resetActiveServer(ctx, tx)
|
||||
src.Active = true
|
||||
|
||||
if v, err := internal.MarshalServer(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(src.ID), v); err != nil {
|
||||
|
@ -106,6 +105,11 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
|
|||
return chronograf.ErrServerNotFound
|
||||
}
|
||||
|
||||
// only one server can be active at a time
|
||||
if src.Active {
|
||||
s.resetActiveServer(ctx, tx)
|
||||
}
|
||||
|
||||
if v, err := internal.MarshalServer(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(src.ID), v); err != nil {
|
||||
|
@ -118,3 +122,39 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServersStore) all(ctx context.Context, tx *bolt.Tx) ([]chronograf.Server, error) {
|
||||
var srcs []chronograf.Server
|
||||
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
|
||||
var src chronograf.Server
|
||||
if err := internal.UnmarshalServer(v, &src); err != nil {
|
||||
return err
|
||||
}
|
||||
srcs = append(srcs, src)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return srcs, err
|
||||
}
|
||||
return srcs, nil
|
||||
}
|
||||
|
||||
// resetActiveServer unsets the Active flag on all sources
|
||||
func (s *ServersStore) resetActiveServer(ctx context.Context, tx *bolt.Tx) error {
|
||||
b := tx.Bucket(ServersBucket)
|
||||
srcs, err := s.all(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, other := range srcs {
|
||||
if other.Active {
|
||||
other.Active = false
|
||||
if v, err := internal.MarshalServer(other); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(other.ID), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ func TestServerStore(t *testing.T) {
|
|||
Username: "marty",
|
||||
Password: "I❤️ jennifer parker",
|
||||
URL: "toyota-hilux.lyon-estates.local",
|
||||
Active: false,
|
||||
},
|
||||
chronograf.Server{
|
||||
Name: "HipToBeSquare",
|
||||
|
@ -34,6 +35,7 @@ func TestServerStore(t *testing.T) {
|
|||
Username: "calvinklein",
|
||||
Password: "chuck b3rry",
|
||||
URL: "toyota-hilux.lyon-estates.local",
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -72,6 +74,21 @@ func TestServerStore(t *testing.T) {
|
|||
t.Fatalf("server 1 update error: got %v, expected %v", src.Name, "Enchantment Under the Sea Dance")
|
||||
}
|
||||
|
||||
// Attempt to make two active sources
|
||||
srcs[0].Active = true
|
||||
srcs[1].Active = true
|
||||
if err := s.Update(ctx, srcs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := s.Update(ctx, srcs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, err := s.Get(ctx, srcs[0].ID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if actual.Active == true {
|
||||
t.Fatal("Able to set two active servers when only one should be permitted")
|
||||
}
|
||||
|
||||
// Delete an server.
|
||||
if err := s.Delete(ctx, srcs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -324,6 +324,7 @@ type Server struct {
|
|||
Username string // Username is the username to connect to the server
|
||||
Password string // Password is in CLEARTEXT
|
||||
URL string // URL are the connections to the server
|
||||
Active bool // Is this the active server for the source?
|
||||
}
|
||||
|
||||
// ServersStore stores connection information for a `Server`
|
||||
|
|
|
@ -195,8 +195,8 @@ Now that we are collecting data with Telegraf and storing data with InfluxDB, it
|
|||
|
||||
#### 1. Download and Install Chronograf
|
||||
```
|
||||
wget https://dl.influxdata.com/chronograf/releases/chronograf_1.2.0~beta8_amd64.deb
|
||||
sudo dpkg -i chronograf_1.2.0~beta8_amd64.deb
|
||||
wget https://dl.influxdata.com/chronograf/releases/chronograf_1.2.0~beta9_amd64.deb
|
||||
sudo dpkg -i chronograf_1.2.0~beta9_amd64.deb
|
||||
```
|
||||
|
||||
#### 2. Start Chronograf
|
||||
|
|
|
@ -26,10 +26,18 @@ type cookie struct {
|
|||
|
||||
// NewCookieJWT creates an Authenticator that uses cookies for auth
|
||||
func NewCookieJWT(secret string, lifespan time.Duration) Authenticator {
|
||||
inactivity := DefaultInactivityDuration
|
||||
// Server interprets a token duration longer than the cookie lifespan as
|
||||
// a token that was issued by a server with a longer auth-duration and is
|
||||
// thus invalid, as a security precaution. So, inactivity must be set to
|
||||
// be less than lifespan.
|
||||
if lifespan > 0 && inactivity > lifespan {
|
||||
inactivity = lifespan / 2 // half of the lifespan ensures tokens can be refreshed once.
|
||||
}
|
||||
return &cookie{
|
||||
Name: DefaultCookieName,
|
||||
Lifespan: lifespan,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Inactivity: inactivity,
|
||||
Now: DefaultNowTime,
|
||||
Tokens: &JWT{
|
||||
Secret: secret,
|
||||
|
@ -44,6 +52,7 @@ func (c *cookie) Validate(ctx context.Context, r *http.Request) (Principal, erro
|
|||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
|
||||
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Lifespan)
|
||||
}
|
||||
|
||||
|
@ -105,15 +114,22 @@ func (c *cookie) setCookie(w http.ResponseWriter, value string, exp time.Time) {
|
|||
|
||||
// Only set a cookie to be persistent (endure beyond the browser session)
|
||||
// if auth duration is greater than zero
|
||||
if c.Lifespan > 0 || exp.Before(c.Now()) {
|
||||
if c.Lifespan > 0 {
|
||||
cookie.Expires = exp
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
// Expire returns a cookie that will expire an existing cookie
|
||||
func (c *cookie) Expire(w http.ResponseWriter) {
|
||||
// to expire cookie set the time in the past
|
||||
c.setCookie(w, "none", c.Now().Add(-1*time.Hour))
|
||||
cookie := http.Cookie{
|
||||
Name: DefaultCookieName,
|
||||
Value: "none",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Expires: c.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
|
|
@ -152,9 +152,25 @@ func TestCookieValidate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewCookieJWT(t *testing.T) {
|
||||
auth := NewCookieJWT("secret", time.Second)
|
||||
if _, ok := auth.(*cookie); !ok {
|
||||
auth := NewCookieJWT("secret", 2*time.Second)
|
||||
if cookie, ok := auth.(*cookie); !ok {
|
||||
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
|
||||
} else if cookie.Inactivity != time.Second {
|
||||
t.Errorf("NewCookieJWT() inactivity was not two seconds: %s", cookie.Inactivity)
|
||||
}
|
||||
|
||||
auth = NewCookieJWT("secret", time.Hour)
|
||||
if cookie, ok := auth.(*cookie); !ok {
|
||||
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
|
||||
} else if cookie.Inactivity != DefaultInactivityDuration {
|
||||
t.Errorf("NewCookieJWT() inactivity was not five minutes: %s", cookie.Inactivity)
|
||||
}
|
||||
|
||||
auth = NewCookieJWT("secret", 0)
|
||||
if cookie, ok := auth.(*cookie); !ok {
|
||||
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
|
||||
} else if cookie.Inactivity != DefaultInactivityDuration {
|
||||
t.Errorf("NewCookieJWT() inactivity was not five minutes: %s", cookie.Inactivity)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ func (c *Claims) Valid() error {
|
|||
}
|
||||
|
||||
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims. lifespan is the
|
||||
// maximum valid lifetime of a token.
|
||||
// maximum valid lifetime of a token. If the lifespan is 0 then the auth lifespan duration is not checked.
|
||||
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error) {
|
||||
gojwt.TimeFunc = j.Now
|
||||
|
||||
|
@ -86,8 +86,9 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
|
|||
|
||||
// If the duration of the claim is longer than the auth lifespan then this is
|
||||
// an invalid claim because server assumes that lifespan is the maximum possible
|
||||
// duration
|
||||
if exp.Sub(iat) > lifespan {
|
||||
// duration. However, a lifespan of zero means that the duration comparison
|
||||
// against the auth duration is not needed.
|
||||
if lifespan > 0 && exp.Sub(iat) > lifespan {
|
||||
return Principal{}, fmt.Errorf("claims duration is different from auth lifespan")
|
||||
}
|
||||
|
||||
|
|
|
@ -79,14 +79,14 @@ func TestAuthenticate(t *testing.T) {
|
|||
{
|
||||
Desc: "Test jwt duration matches auth duration",
|
||||
Secret: "secret",
|
||||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI",
|
||||
Duration: 500 * time.Hour,
|
||||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzMDAsImlhdCI6LTQ0Njc3NDQwMCwiaXNzIjoiaGlsbHZhbGxleSIsIm5iZiI6LTQ0Njc3NDQwMCwic3ViIjoibWFydHlAcGluaGVhZC5uZXQifQ.njEjstpuIDnghSR7VyPPB9QlvJ6Q5JpR3ZEZ_8vGYfA",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "marty@pinhead.net",
|
||||
ExpiresAt: history,
|
||||
IssuedAt: history,
|
||||
IssuedAt: history.Add(100 * time.Second),
|
||||
},
|
||||
Err: errors.New("claims duration is different from auth duration"),
|
||||
Err: errors.New("claims duration is different from auth lifespan"),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
@ -97,6 +97,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
},
|
||||
}
|
||||
principal, err := j.ValidPrincipal(context.Background(), test.Token, test.Duration)
|
||||
if test.Err != nil && err == nil {
|
||||
t.Fatalf("Expected err %s", test.Err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
if test.Err == nil {
|
||||
t.Errorf("Error in test %s authenticating with bad token: %v", test.Desc, err)
|
||||
|
|
|
@ -17,6 +17,7 @@ type postKapacitorRequest struct {
|
|||
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
|
||||
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
|
||||
Password string `json:"password,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (p *postKapacitorRequest) Valid() error {
|
||||
|
@ -47,6 +48,7 @@ type kapacitor struct {
|
|||
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
|
||||
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
|
||||
Password string `json:"password,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
|
||||
}
|
||||
|
||||
|
@ -81,6 +83,7 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
|
|||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
URL: *req.URL,
|
||||
Active: req.Active,
|
||||
}
|
||||
|
||||
if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
|
||||
|
@ -102,6 +105,7 @@ func newKapacitor(srv chronograf.Server) kapacitor {
|
|||
Username: srv.Username,
|
||||
Password: srv.Password,
|
||||
URL: srv.URL,
|
||||
Active: srv.Active,
|
||||
Links: kapaLinks{
|
||||
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
|
||||
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
|
||||
|
@ -217,6 +221,7 @@ type patchKapacitorRequest struct {
|
|||
URL *string `json:"url,omitempty"` // URL for the kapacitor
|
||||
Username *string `json:"username,omitempty"` // Username for kapacitor auth
|
||||
Password *string `json:"password,omitempty"`
|
||||
Active *bool `json:"active"`
|
||||
}
|
||||
|
||||
func (p *patchKapacitorRequest) Valid() error {
|
||||
|
@ -276,6 +281,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
|
|||
if req.Username != nil {
|
||||
srv.Username = *req.Username
|
||||
}
|
||||
if req.Active != nil {
|
||||
srv.Active = *req.Active
|
||||
}
|
||||
|
||||
if err := h.ServersStore.Update(ctx, srv); err != nil {
|
||||
msg := fmt.Sprintf("Error updating kapacitor ID %d", id)
|
||||
|
|
|
@ -1154,7 +1154,7 @@
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"summary": "Configured kapacitors",
|
||||
"summary": "Retrieve list of configured kapacitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of kapacitors",
|
||||
|
@ -1239,7 +1239,7 @@
|
|||
}
|
||||
],
|
||||
"summary": "Configured kapacitors",
|
||||
"description": "These kapacitors are used for monitoring and alerting.",
|
||||
"description": "Retrieve information on a single kapacitor instance",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Kapacitor connection information",
|
||||
|
@ -1334,7 +1334,8 @@
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"summary": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
|
||||
"summary": "Remove Kapacitor backend",
|
||||
"description": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "kapacitor has been removed."
|
||||
|
@ -1683,7 +1684,7 @@
|
|||
"kapacitors",
|
||||
"proxy"
|
||||
],
|
||||
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
|
||||
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -2388,6 +2389,7 @@
|
|||
"id": "4",
|
||||
"name": "kapa",
|
||||
"url": "http://localhost:9092",
|
||||
"active": false,
|
||||
"links": {
|
||||
"proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy",
|
||||
"self": "/chronograf/v1/sources/4/kapacitors/4",
|
||||
|
@ -2417,6 +2419,10 @@
|
|||
"format": "url",
|
||||
"description": "URL for the kapacitor backend (e.g. http://localhost:9092)"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the kapacitor is the current kapacitor being used for a source"
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
},
|
||||
rules: {
|
||||
'quotes': [0, "double"],
|
||||
'quotes': [1, 'single'],
|
||||
'func-style': 0,
|
||||
'func-names': 0,
|
||||
'arrow-parens': 0,
|
||||
|
|
|
@ -106,10 +106,12 @@
|
|||
"react-grid-layout": "^0.13.9",
|
||||
"react-onclickoutside": "^5.2.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^2.4.1",
|
||||
"react-router": "^3.0.2",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-sparklines": "^1.4.2",
|
||||
"react-tooltip": "^3.2.1",
|
||||
"redux": "^3.3.1",
|
||||
"redux-auth-wrapper": "^1.0.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"rome": "^2.1.22",
|
||||
"updeep": "^0.13.0"
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -1,27 +1,20 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import SideNavContainer from 'src/side_nav'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import SideNav from 'src/side_nav'
|
||||
import Notifications from 'shared/components/Notifications'
|
||||
import {
|
||||
publishNotification as publishNotificationAction,
|
||||
} from 'src/shared/actions/notifications'
|
||||
|
||||
import {publishNotification} from 'src/shared/actions/notifications'
|
||||
|
||||
const {
|
||||
func,
|
||||
node,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const App = React.createClass({
|
||||
propTypes: {
|
||||
children: node.isRequired,
|
||||
location: shape({
|
||||
pathname: string,
|
||||
}),
|
||||
params: shape({
|
||||
sourceID: string.isRequired,
|
||||
}).isRequired,
|
||||
notify: func.isRequired,
|
||||
},
|
||||
|
||||
|
@ -32,16 +25,10 @@ const App = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {params: {sourceID}, location} = this.props
|
||||
|
||||
return (
|
||||
<div className="chronograf-root">
|
||||
<SideNavContainer
|
||||
sourceID={sourceID}
|
||||
addFlashMessage={this.handleAddFlashMessage}
|
||||
currentLocation={this.props.location.pathname}
|
||||
/>
|
||||
<Notifications location={location} />
|
||||
<SideNav />
|
||||
<Notifications />
|
||||
{this.props.children && React.cloneElement(this.props.children, {
|
||||
addFlashMessage: this.handleAddFlashMessage,
|
||||
})}
|
||||
|
@ -50,6 +37,8 @@ const App = React.createClass({
|
|||
},
|
||||
})
|
||||
|
||||
export default connect(null, {
|
||||
notify: publishNotificationAction,
|
||||
})(App)
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
notify: bindActionCreators(publishNotification, dispatch),
|
||||
})
|
||||
|
||||
export default connect(null, mapDispatchToProps)(App)
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {getSources} from 'src/shared/apis'
|
||||
import {loadSources as loadSourcesAction} from 'src/shared/actions/sources'
|
||||
import {showDatabases} from 'src/shared/apis/metaQuery'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {getSources} from 'shared/apis'
|
||||
import {showDatabases} from 'shared/apis/metaQuery'
|
||||
|
||||
import {loadSources as loadSourcesAction} from 'shared/actions/sources'
|
||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
|
||||
// Acts as a 'router middleware'. The main `App` component is responsible for
|
||||
// getting the list of data nodes, but not every page requires them to function.
|
||||
// Routes that do require data nodes can be nested under this component.
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
node,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, node, shape, string} = PropTypes
|
||||
const CheckSources = React.createClass({
|
||||
propTypes: {
|
||||
sources: arrayOf(shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
self: string.isRequired,
|
||||
kapacitors: string.isRequired,
|
||||
queries: string.isRequired,
|
||||
permissions: string.isRequired,
|
||||
users: string.isRequired,
|
||||
databases: string.isRequired,
|
||||
}).isRequired,
|
||||
})),
|
||||
sources: arrayOf(
|
||||
shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
self: string.isRequired,
|
||||
kapacitors: string.isRequired,
|
||||
queries: string.isRequired,
|
||||
permissions: string.isRequired,
|
||||
users: string.isRequired,
|
||||
databases: string.isRequired,
|
||||
}).isRequired,
|
||||
})
|
||||
),
|
||||
addFlashMessage: func,
|
||||
children: node,
|
||||
params: shape({
|
||||
|
@ -39,7 +39,8 @@ const CheckSources = React.createClass({
|
|||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
loadSourcesAction: func.isRequired,
|
||||
loadSources: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
|
@ -58,7 +59,7 @@ const CheckSources = React.createClass({
|
|||
|
||||
getChildContext() {
|
||||
const {sources, params: {sourceID}} = this.props
|
||||
return {source: sources.find((s) => s.id === sourceID)}
|
||||
return {source: sources.find(s => s.id === sourceID)}
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -67,56 +68,78 @@ const CheckSources = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
getSources().then(({data: {sources}}) => {
|
||||
this.props.loadSourcesAction(sources)
|
||||
async componentWillMount() {
|
||||
const {loadSources, errorThrown} = this.props
|
||||
|
||||
try {
|
||||
const {data: {sources}} = await getSources()
|
||||
loadSources(sources)
|
||||
this.setState({isFetching: false})
|
||||
}).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: "Unable to connect to Chronograf server"})
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to connect to Chronograf server')
|
||||
this.setState({isFetching: false})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
const {router, location, params, addFlashMessage, sources} = nextProps
|
||||
async componentWillUpdate(nextProps, nextState) {
|
||||
const {router, location, params, errorThrown, sources} = nextProps
|
||||
const {isFetching} = nextState
|
||||
const source = sources.find((s) => s.id === params.sourceID)
|
||||
const defaultSource = sources.find((s) => s.default === true)
|
||||
const source = sources.find(s => s.id === params.sourceID)
|
||||
const defaultSource = sources.find(s => s.default === true)
|
||||
|
||||
if (!isFetching && !source) {
|
||||
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
||||
const restString = rest === null ? 'hosts' : rest[1]
|
||||
|
||||
if (defaultSource) {
|
||||
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
||||
return router.push(`/sources/${defaultSource.id}/${rest[1]}`)
|
||||
return router.push(`/sources/${defaultSource.id}/${restString}`)
|
||||
} else if (sources[0]) {
|
||||
return router.push(`/sources/${sources[0].id}/${restString}`)
|
||||
}
|
||||
|
||||
return router.push(`/sources/new?redirectPath=${location.pathname}`)
|
||||
}
|
||||
|
||||
if (!isFetching && !location.pathname.includes("/manage-sources")) {
|
||||
if (!isFetching && !location.pathname.includes('/manage-sources')) {
|
||||
// Do simple query to proxy to see if the source is up.
|
||||
showDatabases(source.links.proxy).catch(() => {
|
||||
addFlashMessage({type: 'error', text: `Unable to connect to source`})
|
||||
})
|
||||
try {
|
||||
await showDatabases(source.links.proxy)
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to connect to source')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const {params, sources} = this.props
|
||||
const {isFetching} = this.state
|
||||
const source = sources.find((s) => s.id === params.sourceID)
|
||||
const source = sources.find(s => s.id === params.sourceID)
|
||||
|
||||
if (isFetching || !source) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return this.props.children && React.cloneElement(this.props.children, Object.assign({}, this.props, {
|
||||
source,
|
||||
}))
|
||||
return (
|
||||
this.props.children &&
|
||||
React.cloneElement(
|
||||
this.props.children,
|
||||
Object.assign({}, this.props, {
|
||||
source,
|
||||
})
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
sources: state.sources,
|
||||
}
|
||||
}
|
||||
const mapStateToProps = ({sources}) => ({
|
||||
sources,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, {loadSourcesAction})(withRouter(CheckSources))
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadSources: bindActionCreators(loadSourcesAction, dispatch),
|
||||
errorThrown: bindActionCreators(errorThrownAction, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
withRouter(CheckSources)
|
||||
)
|
||||
|
|
|
@ -20,8 +20,8 @@ import {
|
|||
killQuery as killQueryProxy,
|
||||
} from 'shared/apis/metaQuery'
|
||||
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {REVERT_STATE_DELAY} from 'shared/constants'
|
||||
|
||||
|
@ -221,23 +221,39 @@ export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
|
|||
|
||||
// async actions
|
||||
export const loadUsersAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getUsersAJAX(url)
|
||||
dispatch(loadUsers(data))
|
||||
try {
|
||||
const {data} = await getUsersAJAX(url)
|
||||
dispatch(loadUsers(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadRolesAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getRolesAJAX(url)
|
||||
dispatch(loadRoles(data))
|
||||
try {
|
||||
const {data} = await getRolesAJAX(url)
|
||||
dispatch(loadRoles(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadPermissionsAsync = (url) => async (dispatch) => {
|
||||
const {data} = await getPermissionsAJAX(url)
|
||||
dispatch(loadPermissions(data))
|
||||
try {
|
||||
const {data} = await getPermissionsAJAX(url)
|
||||
dispatch(loadPermissions(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadDBsAndRPsAsync = (url) => async (dispatch) => {
|
||||
const {data: {databases}} = await getDbsAndRpsAJAX(url)
|
||||
dispatch(loadDatabases(databases))
|
||||
try {
|
||||
const {data: {databases}} = await getDbsAndRpsAJAX(url)
|
||||
dispatch(loadDatabases(databases))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const createUserAsync = (url, user) => async (dispatch) => {
|
||||
|
@ -246,8 +262,8 @@ export const createUserAsync = (url, user) => async (dispatch) => {
|
|||
dispatch(publishAutoDismissingNotification('success', 'User created successfully'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error, `Failed to create user: ${error.data.message}`))
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create user: ${error.data.message}`))
|
||||
setTimeout(() => dispatch(deleteUser(user)), REVERT_STATE_DELAY)
|
||||
}
|
||||
}
|
||||
|
@ -258,8 +274,8 @@ export const createRoleAsync = (url, role) => async (dispatch) => {
|
|||
dispatch(publishAutoDismissingNotification('success', 'Role created successfully'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error, `Failed to create role: ${error.data.message}`))
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create role: ${error.data.message}`))
|
||||
setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY)
|
||||
}
|
||||
}
|
||||
|
@ -270,8 +286,8 @@ export const createDatabaseAsync = (url, database) => async (dispatch) => {
|
|||
dispatch(syncDatabase(database, data))
|
||||
dispatch(publishAutoDismissingNotification('success', 'Database created successfully'))
|
||||
} catch (error) {
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create database: ${error.data.message}`))
|
||||
dispatch(errorThrown(error))
|
||||
// undo optimistic upda, `Failed to create database: ${error.data.message}`te
|
||||
setTimeout(() => dispatch(removeDatabase(database)), REVERT_STATE_DELAY)
|
||||
}
|
||||
}
|
||||
|
@ -282,8 +298,8 @@ export const createRetentionPolicyAsync = (database, retentionPolicy) => async (
|
|||
dispatch(publishAutoDismissingNotification('success', 'Retention policy created successfully'))
|
||||
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
|
||||
} catch (error) {
|
||||
// undo optimistic update
|
||||
dispatch(publishNotification('error', `Failed to create retention policy: ${error.data.message}`))
|
||||
dispatch(errorThrown(error))
|
||||
// undo optimistic upda, `Failed to create retention policy: ${error.data.message}`te
|
||||
setTimeout(() => dispatch(removeRetentionPolicy(database, retentionPolicy)), REVERT_STATE_DELAY)
|
||||
}
|
||||
}
|
||||
|
@ -295,17 +311,21 @@ export const updateRetentionPolicyAsync = (database, retentionPolicy, updates) =
|
|||
dispatch(publishAutoDismissingNotification('success', 'Retention policy updated successfully'))
|
||||
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update retention policy: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update retention policy: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const killQueryAsync = (source, queryID) => (dispatch) => {
|
||||
export const killQueryAsync = (source, queryID) => async (dispatch) => {
|
||||
// optimistic update
|
||||
dispatch(killQuery(queryID))
|
||||
dispatch(setQueryToKill(null))
|
||||
|
||||
// kill query on server
|
||||
killQueryProxy(source, queryID)
|
||||
try {
|
||||
// kill query on server
|
||||
await killQueryProxy(source, queryID)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
// TODO: handle failed killQuery
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRoleAsync = (role) => async (dispatch) => {
|
||||
|
@ -314,7 +334,7 @@ export const deleteRoleAsync = (role) => async (dispatch) => {
|
|||
await deleteRoleAJAX(role.links.self)
|
||||
dispatch(publishAutoDismissingNotification('success', 'Role deleted'))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to delete role: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to delete role: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,7 +344,7 @@ export const deleteUserAsync = (user) => async (dispatch) => {
|
|||
await deleteUserAJAX(user.links.self)
|
||||
dispatch(publishAutoDismissingNotification('success', 'User deleted'))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to delete user: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to delete user: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,7 +354,7 @@ export const deleteDatabaseAsync = (database) => async (dispatch) => {
|
|||
await deleteDatabaseAJAX(database.links.self)
|
||||
dispatch(publishAutoDismissingNotification('success', 'Database deleted'))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to delete database: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to delete database: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,7 +364,7 @@ export const deleteRetentionPolicyAsync = (database, retentionPolicy) => async (
|
|||
await deleteRetentionPolicyAJAX(retentionPolicy.links.self)
|
||||
dispatch(publishAutoDismissingNotification('success', `Retention policy ${retentionPolicy.name} deleted`))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to delete retentionPolicy: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to delete retentionPolicy: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,7 +374,7 @@ export const updateRoleUsersAsync = (role, users) => async (dispatch) => {
|
|||
dispatch(publishAutoDismissingNotification('success', 'Role users updated'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update role: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -364,7 +384,7 @@ export const updateRolePermissionsAsync = (role, permissions) => async (dispatch
|
|||
dispatch(publishAutoDismissingNotification('success', 'Role permissions updated'))
|
||||
dispatch(syncRole(role, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update role: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,7 +394,7 @@ export const updateUserPermissionsAsync = (user, permissions) => async (dispatch
|
|||
dispatch(publishAutoDismissingNotification('success', 'User permissions updated'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,7 +404,7 @@ export const updateUserRolesAsync = (user, roles) => async (dispatch) => {
|
|||
dispatch(publishAutoDismissingNotification('success', 'User roles updated'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -394,6 +414,6 @@ export const updateUserPasswordAsync = (user, password) => async (dispatch) => {
|
|||
dispatch(publishAutoDismissingNotification('success', 'User password updated'))
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(publishNotification('error', `Failed to update user: ${error.data.message}`))
|
||||
dispatch(errorThrown(error, `Failed to update user: ${error.data.message}`))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ const RoleRow = ({
|
|||
<RoleEditingRow role={role} onEdit={onEdit} onSave={onSave} isNew={isNew} />
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<td className="text-right" style={{width: '85px'}}>
|
||||
<ConfirmButtons item={role} onConfirm={onSave} onCancel={onCancel} />
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -42,7 +42,7 @@ const UserRow = ({
|
|||
<UserEditingRow user={user} onEdit={onEdit} onSave={onSave} isNew={isNew} />
|
||||
{hasRoles ? <td></td> : null}
|
||||
<td></td>
|
||||
<td className="text-right" style={{width: "85px"}}>
|
||||
<td className="text-right" style={{width: '85px'}}>
|
||||
<ConfirmButtons item={user} onConfirm={onSave} onCancel={onCancel} />
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -75,7 +75,7 @@ const UserRow = ({
|
|||
/> : null
|
||||
}
|
||||
</td>
|
||||
<td className="text-right" style={{width: "300px"}}>
|
||||
<td className="text-right" style={{width: '300px'}}>
|
||||
<ChangePassRow onEdit={onEdit} onApply={handleUpdatePassword} user={user} />
|
||||
</td>
|
||||
<DeleteConfirmTableCell onDelete={onDelete} item={user} />
|
||||
|
|
|
@ -4,6 +4,6 @@ export function getAlerts(source, timeRange) {
|
|||
return proxy({
|
||||
source,
|
||||
query: `SELECT host, value, level, alertName FROM alerts WHERE time >= '${timeRange.lower}' AND time <= '${timeRange.upper}' ORDER BY time desc`,
|
||||
db: "chronograf",
|
||||
db: 'chronograf',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -1,33 +1,51 @@
|
|||
/* global VERSION */
|
||||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
const {array} = PropTypes
|
||||
import Notifications from 'shared/components/Notifications'
|
||||
|
||||
const Login = ({auth}) => (
|
||||
<div className="auth-page">
|
||||
<div className="auth-box">
|
||||
<div className="auth-logo"></div>
|
||||
<h1 className="auth-text-logo">Chronograf</h1>
|
||||
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p>
|
||||
{auth.map(({name, login, label}) => (
|
||||
<a key={name} className="btn btn-primary" href={login}>
|
||||
<span className={`icon ${name}`}></span>
|
||||
Login with {label}
|
||||
</a>
|
||||
))}
|
||||
const Login = ({authData: {auth}}) => {
|
||||
if (auth.isAuthLoading) {
|
||||
return <div className="page-spinner"></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Notifications />
|
||||
<div className="auth-page">
|
||||
<div className="auth-box">
|
||||
<div className="auth-logo"></div>
|
||||
<h1 className="auth-text-logo">Chronograf</h1>
|
||||
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p>
|
||||
{auth.links && auth.links.map(({name, login, label}) => (
|
||||
<a key={name} className="btn btn-primary" href={login}>
|
||||
<span className={`icon ${name}`}></span>
|
||||
Login with {label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<p className="auth-credits">Made by <span className="icon cubo-uniform"></span>InfluxData</p>
|
||||
<div className="auth-image"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="auth-credits">Made by <span className="icon cubo-uniform"></span>InfluxData</p>
|
||||
<div className="auth-image"></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Login.propTypes = {
|
||||
auth: array.isRequired,
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
})
|
||||
const {
|
||||
array,
|
||||
bool,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
export default connect(mapStateToProps)(Login)
|
||||
Login.propTypes = {
|
||||
authData: shape({
|
||||
me: shape(),
|
||||
links: array,
|
||||
isLoading: bool,
|
||||
}),
|
||||
location: shape({
|
||||
pathname: string,
|
||||
}),
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
import Login from './Login'
|
||||
export {Login}
|
||||
import {UserIsAuthenticated, Authenticated, UserIsNotAuthenticated} from './Authenticated'
|
||||
export {Login, UserIsAuthenticated, Authenticated, UserIsNotAuthenticated}
|
||||
|
|
|
@ -7,9 +7,8 @@ import {
|
|||
deleteDashboardCell as deleteDashboardCellAJAX,
|
||||
} from 'src/dashboards/apis'
|
||||
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
// import {errorThrown} from 'shared/actions/errors'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||
|
||||
|
@ -138,6 +137,7 @@ export const getDashboardsAsync = () => async dispatch => {
|
|||
const {data: {dashboards}} = await getDashboardsAJAX()
|
||||
dispatch(loadDashboards(dashboards))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ export const updateDashboardCell = (dashboard, cell) => async dispatch => {
|
|||
dispatch(syncDashboardCell(dashboard, data))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// dispatch(errorThrown(error))
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,13 +174,10 @@ export const deleteDashboardAsync = dashboard => async dispatch => {
|
|||
)
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(deleteDashboardFailed(dashboard))
|
||||
dispatch(
|
||||
publishNotification(
|
||||
'error',
|
||||
`Failed to delete dashboard: ${error.data.message}.`
|
||||
)
|
||||
errorThrown(error, `Failed to delete dashboard: ${error.data.message}.`)
|
||||
)
|
||||
dispatch(deleteDashboardFailed(dashboard))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,6 +189,7 @@ export const addDashboardCellAsync = dashboard => async dispatch => {
|
|||
)
|
||||
dispatch(addDashboardCell(dashboard, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
|
@ -202,7 +200,7 @@ export const deleteDashboardCellAsync = cell => async dispatch => {
|
|||
await deleteDashboardCellAJAX(cell)
|
||||
dispatch(deleteDashboardCell(cell))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(errorThrown(error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ import React, {Component, PropTypes} from 'react'
|
|||
import _ from 'lodash'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
|
||||
import QueryBuilder from 'src/data_explorer/components/QueryBuilder'
|
||||
import ResizeContainer, {
|
||||
ResizeBottom,
|
||||
} from 'src/shared/components/ResizeContainer'
|
||||
import QueryMaker from 'src/data_explorer/components/QueryMaker'
|
||||
import Visualization from 'src/data_explorer/components/Visualization'
|
||||
import OverlayControls from 'src/dashboards/components/OverlayControls'
|
||||
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||
|
@ -30,7 +32,9 @@ class CellEditorOverlay extends Component {
|
|||
|
||||
const {cell: {name, type, queries}} = props
|
||||
|
||||
const queriesWorkingDraft = _.cloneDeep(queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()})))
|
||||
const queriesWorkingDraft = _.cloneDeep(
|
||||
queries.map(({queryConfig}) => ({...queryConfig, id: uuid.v4()}))
|
||||
)
|
||||
|
||||
this.state = {
|
||||
cellWorkingName: name,
|
||||
|
@ -45,7 +49,9 @@ class CellEditorOverlay extends Component {
|
|||
const nextStatus = nextProps.queryStatus
|
||||
if (nextStatus.status && nextStatus.queryID) {
|
||||
if (nextStatus.queryID !== queryID || nextStatus.status !== status) {
|
||||
const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === queryID ? ({...q, status: nextStatus.status}) : q)
|
||||
const nextQueries = this.state.queriesWorkingDraft.map(
|
||||
q => (q.id === queryID ? {...q, status: nextStatus.status} : q)
|
||||
)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
}
|
||||
|
@ -54,11 +60,13 @@ class CellEditorOverlay extends Component {
|
|||
queryStateReducer(queryModifier) {
|
||||
return (queryID, payload) => {
|
||||
const {queriesWorkingDraft} = this.state
|
||||
const query = queriesWorkingDraft.find((q) => q.id === queryID)
|
||||
const query = queriesWorkingDraft.find(q => q.id === queryID)
|
||||
|
||||
const nextQuery = queryModifier(query, payload)
|
||||
|
||||
const nextQueries = queriesWorkingDraft.map((q) => q.id === query.id ? nextQuery : q)
|
||||
const nextQueries = queriesWorkingDraft.map(
|
||||
q => (q.id === query.id ? nextQuery : q)
|
||||
)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +78,9 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleDeleteQuery(index) {
|
||||
const nextQueries = this.state.queriesWorkingDraft.filter((__, i) => i !== index)
|
||||
const nextQueries = this.state.queriesWorkingDraft.filter(
|
||||
(__, i) => i !== index
|
||||
)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
}
|
||||
|
||||
|
@ -81,11 +91,9 @@ class CellEditorOverlay extends Component {
|
|||
const newCell = _.cloneDeep(cell)
|
||||
newCell.name = cellWorkingName
|
||||
newCell.type = cellWorkingType
|
||||
newCell.queries = queriesWorkingDraft.map((q) => {
|
||||
newCell.queries = queriesWorkingDraft.map(q => {
|
||||
const query = q.rawText || buildInfluxQLQuery(timeRange, q)
|
||||
const label = q.rawText ?
|
||||
"" :
|
||||
`${q.measurement}.${q.fields[0].field}`
|
||||
const label = q.rawText ? '' : `${q.measurement}.${q.fields[0].field}`
|
||||
|
||||
return {
|
||||
queryConfig: q,
|
||||
|
@ -110,7 +118,9 @@ class CellEditorOverlay extends Component {
|
|||
try {
|
||||
const {data} = await getQueryConfig(url, [{query: text, id}])
|
||||
const config = data.queries.find(q => q.id === id)
|
||||
const nextQueries = this.state.queriesWorkingDraft.map((q) => q.id === id ? config.queryConfig : q)
|
||||
const nextQueries = this.state.queriesWorkingDraft.map(
|
||||
q => (q.id === id ? config.queryConfig : q)
|
||||
)
|
||||
this.setState({queriesWorkingDraft: nextQueries})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -136,7 +146,7 @@ class CellEditorOverlay extends Component {
|
|||
const queryActions = {
|
||||
addQuery: this.handleAddQuery,
|
||||
editRawTextAsync: this.handleEditRawText,
|
||||
..._.mapValues(queryModifiers, (qm) => this.queryStateReducer(qm)),
|
||||
..._.mapValues(queryModifiers, qm => this.queryStateReducer(qm)),
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -150,16 +160,19 @@ class CellEditorOverlay extends Component {
|
|||
cellType={cellWorkingType}
|
||||
cellName={cellWorkingName}
|
||||
editQueryStatus={editQueryStatus}
|
||||
views={[]}
|
||||
/>
|
||||
<ResizeBottom>
|
||||
<div style={{display: 'flex', flexDirection: 'column', height: '100%'}}>
|
||||
<div
|
||||
style={{display: 'flex', flexDirection: 'column', height: '100%'}}
|
||||
>
|
||||
<OverlayControls
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onCancel={onCancel}
|
||||
onSave={this.handleSaveCell}
|
||||
/>
|
||||
<QueryBuilder
|
||||
<QueryMaker
|
||||
source={source}
|
||||
queries={queriesWorkingDraft}
|
||||
actions={queryActions}
|
||||
|
@ -177,12 +190,7 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {func, number, shape, string} = PropTypes
|
||||
|
||||
CellEditorOverlay.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
|
|
|
@ -9,7 +9,7 @@ const OverlayControls = (props) => {
|
|||
const {onCancel, onSave, selectedGraphType, onSelectGraphType} = props
|
||||
return (
|
||||
<div className="overlay-controls">
|
||||
<h3 className="overlay--graph-name">Graph Editor</h3>
|
||||
<h3 className="overlay--graph-name">Cell Editor</h3>
|
||||
<div className="overlay-controls--right">
|
||||
<p>Visualization Type:</p>
|
||||
<ul className="toggle toggle-sm">
|
||||
|
|
|
@ -56,9 +56,9 @@ const DashboardsPage = React.createClass({
|
|||
const dashboardLink = `/sources/${this.props.source.id}`
|
||||
let tableHeader
|
||||
if (dashboards === null) {
|
||||
tableHeader = "Loading Dashboards..."
|
||||
tableHeader = 'Loading Dashboards...'
|
||||
} else if (dashboards.length === 0) {
|
||||
tableHeader = "1 Dashboard"
|
||||
tableHeader = '1 Dashboard'
|
||||
} else {
|
||||
tableHeader = `${dashboards.length + 1} Dashboards`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import uuid from 'node-uuid'
|
||||
|
||||
import {getQueryConfig} from 'shared/apis'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
export function addQuery(options = {}) {
|
||||
return {
|
||||
type: 'ADD_QUERY',
|
||||
|
@ -137,15 +140,13 @@ export const updateQueryConfig = (config) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export function editQueryStatus(queryID, status) {
|
||||
return {
|
||||
type: 'EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const editQueryStatus = (queryID, status) => ({
|
||||
type: 'EDIT_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
status,
|
||||
},
|
||||
})
|
||||
|
||||
// Async actions
|
||||
export const editRawTextAsync = (url, id, text) => async (dispatch) => {
|
||||
|
@ -154,6 +155,6 @@ export const editRawTextAsync = (url, id, text) => async (dispatch) => {
|
|||
const config = data.queries.find(q => q.id === id)
|
||||
dispatch(updateQueryConfig(config.queryConfig))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,19 +61,19 @@ const DatabaseList = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Databases</div>
|
||||
<ul className="qeditor--list">
|
||||
<div className="query-builder--heading">Databases</div>
|
||||
<div className="query-builder--list">
|
||||
{this.state.namespaces.map((namespace) => {
|
||||
const {database, retentionPolicy} = namespace
|
||||
const isActive = database === query.database && retentionPolicy === query.retentionPolicy
|
||||
|
||||
return (
|
||||
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
|
||||
<div className={classNames('query-builder--list-item', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
|
||||
{database}.{retentionPolicy}
|
||||
</li>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -74,17 +74,12 @@ const FieldList = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Fields</div>
|
||||
{
|
||||
hasAggregates ?
|
||||
<div className="qeditor--list-header">
|
||||
<div className="group-by-time">
|
||||
<p>Group by Time</p>
|
||||
<GroupByTimeDropdown isOpen={!hasGroupByTime} selected={query.groupBy.time} onChooseGroupByTime={this.handleGroupByTime} />
|
||||
</div>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div className="query-builder--heading">
|
||||
<span>Fields</span>
|
||||
{hasAggregates ?
|
||||
<GroupByTimeDropdown isOpen={!hasGroupByTime} selected={query.groupBy.time} onChooseGroupByTime={this.handleGroupByTime} />
|
||||
: null}
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
)
|
||||
|
@ -93,11 +88,15 @@ const FieldList = React.createClass({
|
|||
renderList() {
|
||||
const {database, measurement} = this.props.query
|
||||
if (!database || !measurement) {
|
||||
return <div className="qeditor--empty">No <strong>Measurement</strong> selected</div>
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Measurement</strong> selected</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="qeditor--list">
|
||||
<div className="query-builder--list">
|
||||
{this.state.fields.map((fieldFunc) => {
|
||||
const selectedField = this.props.query.fields.find((f) => f.field === fieldFunc.field)
|
||||
return (
|
||||
|
@ -111,7 +110,7 @@ const FieldList = React.createClass({
|
|||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -39,16 +39,17 @@ const FieldListItem = React.createClass({
|
|||
})
|
||||
|
||||
return (
|
||||
<li className={classNames("qeditor--list-item qeditor--list-checkbox", {checked: isSelected})} key={fieldFunc} onClick={_.wrap(fieldFunc, this.handleToggleField)}>
|
||||
<span className="qeditor--list-checkbox__checkbox">{fieldText}</span>
|
||||
<div className="qeditor--hidden-dropdown">
|
||||
{
|
||||
isKapacitorRule ?
|
||||
<Dropdown items={items} onChoose={this.handleApplyFunctions} selected={fieldFunc.funcs.length ? fieldFunc.funcs[0] : 'Select a function'} /> :
|
||||
<MultiSelectDropdown items={INFLUXQL_FUNCTIONS} onApply={this.handleApplyFunctions} selectedItems={fieldFunc.funcs || []} />
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
<div className={classNames('query-builder--list-item', {active: isSelected})} key={fieldFunc} onClick={_.wrap(fieldFunc, this.handleToggleField)}>
|
||||
<span>
|
||||
<div className="query-builder--checkbox"></div>
|
||||
{fieldText}
|
||||
</span>
|
||||
{
|
||||
isKapacitorRule ?
|
||||
<Dropdown items={items} onChoose={this.handleApplyFunctions} selected={fieldFunc.funcs.length ? fieldFunc.funcs[0] : 'Function'} /> :
|
||||
<MultiSelectDropdown items={INFLUXQL_FUNCTIONS} onApply={this.handleApplyFunctions} selectedItems={fieldFunc.funcs || []} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -16,16 +16,16 @@ const GroupByTimeDropdown = React.createClass({
|
|||
const {isOpen, selected, onChooseGroupByTime} = this.props
|
||||
|
||||
return (
|
||||
<div className="dropdown group-by-time-dropdown">
|
||||
<div className={classNames('dropdown group-by-time-dropdown', {open: isOpen})}>
|
||||
<div className="btn btn-sm btn-info dropdown-toggle" data-toggle="dropdown">
|
||||
<span className="selected-group-by">{selected || '...'}</span>
|
||||
<span className="caret" />
|
||||
<span>Group by {selected || 'time'}</span>
|
||||
<span className="caret"></span>
|
||||
</div>
|
||||
<ul className={classNames("dropdown-menu", {show: isOpen})} aria-labelledby="group-by-dropdown">
|
||||
<ul className="dropdown-menu">
|
||||
{groupByTimeOptions.map((groupBy) => {
|
||||
return (
|
||||
<li key={groupBy.menuOption}>
|
||||
<a href="#" onClick={() => onChooseGroupByTime(groupBy)}>
|
||||
<li className="dropdown-item" key={groupBy.menuOption}onClick={() => onChooseGroupByTime(groupBy)}>
|
||||
<a href="#">
|
||||
{groupBy.menuOption}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -25,7 +25,7 @@ const MeasurementList = React.createClass({
|
|||
getInitialState() {
|
||||
return {
|
||||
measurements: [],
|
||||
filterText: "",
|
||||
filterText: '',
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -72,11 +72,15 @@ const MeasurementList = React.createClass({
|
|||
render() {
|
||||
return (
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Measurements</div>
|
||||
{this.props.query.database ? <div className="qeditor--list-header">
|
||||
<input className="qeditor--filter" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<span className="icon search"></span>
|
||||
</div> : null }
|
||||
<div className="query-builder--heading">
|
||||
<span>Measurements</span>
|
||||
{this.props.query.database ?
|
||||
<div className="query-builder--filter">
|
||||
<input className="form-control input-sm" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<span className="icon search"></span>
|
||||
</div>
|
||||
: null }
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
)
|
||||
|
@ -84,20 +88,24 @@ const MeasurementList = React.createClass({
|
|||
|
||||
renderList() {
|
||||
if (!this.props.query.database) {
|
||||
return <div className="qeditor--empty">No <strong>Database</strong> selected</div>
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Database</strong> selected</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const measurements = this.state.measurements.filter((m) => m.match(this.state.filterText))
|
||||
|
||||
return (
|
||||
<ul className="qeditor--list">
|
||||
<div className="query-builder--list">
|
||||
{measurements.map((measurement) => {
|
||||
const isActive = measurement === this.props.query.measurement
|
||||
return (
|
||||
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={measurement} onClick={_.wrap(measurement, this.props.onChooseMeasurement)}>{measurement}</li>
|
||||
<div className={classNames('query-builder--list-item', {active: isActive})} key={measurement} onClick={_.wrap(measurement, this.props.onChooseMeasurement)}>{measurement}</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import DatabaseList from './DatabaseList'
|
||||
import MeasurementList from './MeasurementList'
|
||||
import FieldList from './FieldList'
|
||||
import TagList from './TagList'
|
||||
import QueryEditor from './QueryEditor'
|
||||
import QueryTabItem from './QueryTabItem'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
node,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
const QueryBuilder = React.createClass({
|
||||
|
@ -20,7 +20,9 @@ const QueryBuilder = React.createClass({
|
|||
queries: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
queries: arrayOf(shape({})).isRequired,
|
||||
query: shape({
|
||||
id: string,
|
||||
}).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
|
@ -28,103 +30,91 @@ const QueryBuilder = React.createClass({
|
|||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
addQuery: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
editRawTextAsync: func.isRequired,
|
||||
}).isRequired,
|
||||
height: string,
|
||||
top: string,
|
||||
setActiveQueryIndex: func.isRequired,
|
||||
onDeleteQuery: func.isRequired,
|
||||
activeQueryIndex: number,
|
||||
children: node,
|
||||
},
|
||||
|
||||
handleAddQuery() {
|
||||
const newIndex = this.props.queries.length
|
||||
this.props.actions.addQuery()
|
||||
this.props.setActiveQueryIndex(newIndex)
|
||||
handleChooseNamespace(namespace) {
|
||||
this.props.actions.chooseNamespace(this.props.query.id, namespace)
|
||||
},
|
||||
|
||||
handleAddRawQuery() {
|
||||
const newIndex = this.props.queries.length
|
||||
this.props.actions.addQuery({rawText: ''})
|
||||
this.props.setActiveQueryIndex(newIndex)
|
||||
handleChooseMeasurement(measurement) {
|
||||
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
|
||||
},
|
||||
|
||||
getActiveQuery() {
|
||||
const {queries, activeQueryIndex} = this.props
|
||||
const activeQuery = queries[activeQueryIndex]
|
||||
const defaultQuery = queries[0]
|
||||
handleToggleField(field) {
|
||||
this.props.actions.toggleField(this.props.query.id, field)
|
||||
},
|
||||
|
||||
return activeQuery || defaultQuery
|
||||
handleGroupByTime(time) {
|
||||
this.props.actions.groupByTime(this.props.query.id, time)
|
||||
},
|
||||
|
||||
handleApplyFuncsToField(fieldFunc) {
|
||||
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc)
|
||||
},
|
||||
|
||||
handleChooseTag(tag) {
|
||||
this.props.actions.chooseTag(this.props.query.id, tag)
|
||||
},
|
||||
|
||||
handleToggleTagAcceptance() {
|
||||
this.props.actions.toggleTagAcceptance(this.props.query.id)
|
||||
},
|
||||
|
||||
handleGroupByTag(tagKey) {
|
||||
this.props.actions.groupByTag(this.props.query.id, tagKey)
|
||||
},
|
||||
|
||||
handleEditRawText(text) {
|
||||
const {source: {links}, query} = this.props
|
||||
this.props.actions.editRawTextAsync(links.queries, query.id, text)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {height, top} = this.props
|
||||
const {query, timeRange} = this.props
|
||||
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
|
||||
|
||||
return (
|
||||
<div className="query-builder" style={{height, top}}>
|
||||
{this.renderQueryTabList()}
|
||||
{this.renderQueryEditor()}
|
||||
<div className="query-maker--tab-contents">
|
||||
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
|
||||
{this.renderLists()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderQueryEditor() {
|
||||
const {timeRange, actions, source} = this.props
|
||||
const query = this.getActiveQuery()
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="qeditor--empty">
|
||||
<h5 className="no-user-select">This Graph has no Queries</h5>
|
||||
<br/>
|
||||
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderLists() {
|
||||
const {query} = this.props
|
||||
|
||||
return (
|
||||
<QueryEditor
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
query={query}
|
||||
actions={actions}
|
||||
onAddQuery={this.handleAddQuery}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
renderQueryTabList() {
|
||||
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props
|
||||
|
||||
return (
|
||||
<div className="query-builder--tabs">
|
||||
<div className="query-builder--tabs-heading">
|
||||
<h1>Queries</h1>
|
||||
<div className="panel--tab-new btn btn-sm btn-primary dropdown-toggle" onClick={this.handleAddQuery}>
|
||||
<span className="icon plus"></span>
|
||||
</div>
|
||||
</div>
|
||||
{queries.map((q, i) => {
|
||||
return (
|
||||
<QueryTabItem
|
||||
isActive={i === activeQueryIndex}
|
||||
key={i}
|
||||
queryIndex={i}
|
||||
query={q}
|
||||
onSelect={setActiveQueryIndex}
|
||||
onDelete={onDeleteQuery}
|
||||
queryTabText={q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{this.props.children}
|
||||
<div className="query-builder">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
/>
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -1,122 +1,120 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classNames from 'classnames'
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import LoadingDots from 'src/shared/components/LoadingDots'
|
||||
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
||||
|
||||
import DatabaseList from './DatabaseList'
|
||||
import MeasurementList from './MeasurementList'
|
||||
import FieldList from './FieldList'
|
||||
import TagList from './TagList'
|
||||
import RawQueryEditor from './RawQueryEditor'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
const {
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
const ENTER = 13
|
||||
const ESCAPE = 27
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
const QueryEditor = React.createClass({
|
||||
propTypes: {
|
||||
source: shape({
|
||||
links: shape({
|
||||
queries: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
query: shape({
|
||||
id: string,
|
||||
}).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
editRawTextAsync: func.isRequired,
|
||||
query: string.isRequired,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape({
|
||||
status: shape({
|
||||
error: string,
|
||||
loading: bool,
|
||||
success: string,
|
||||
warn: string,
|
||||
}),
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
handleChooseNamespace(namespace) {
|
||||
this.props.actions.chooseNamespace(this.props.query.id, namespace)
|
||||
getInitialState() {
|
||||
return {
|
||||
value: this.props.query,
|
||||
}
|
||||
},
|
||||
|
||||
handleChooseMeasurement(measurement) {
|
||||
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query !== nextProps.query) {
|
||||
this.setState({value: nextProps.query})
|
||||
}
|
||||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
this.props.actions.toggleField(this.props.query.id, field)
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === ENTER) {
|
||||
e.preventDefault()
|
||||
this.handleUpdate()
|
||||
} else if (e.keyCode === ESCAPE) {
|
||||
this.setState({value: this.state.value}, () => {
|
||||
this.editor.blur()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleGroupByTime(time) {
|
||||
this.props.actions.groupByTime(this.props.query.id, time)
|
||||
handleChange() {
|
||||
this.setState({
|
||||
value: this.editor.value,
|
||||
})
|
||||
},
|
||||
|
||||
handleApplyFuncsToField(fieldFunc) {
|
||||
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc)
|
||||
handleUpdate() {
|
||||
this.props.onUpdate(this.state.value)
|
||||
},
|
||||
|
||||
handleChooseTag(tag) {
|
||||
this.props.actions.chooseTag(this.props.query.id, tag)
|
||||
},
|
||||
|
||||
handleToggleTagAcceptance() {
|
||||
this.props.actions.toggleTagAcceptance(this.props.query.id)
|
||||
},
|
||||
|
||||
handleGroupByTag(tagKey) {
|
||||
this.props.actions.groupByTag(this.props.query.id, tagKey)
|
||||
},
|
||||
|
||||
handleEditRawText(text) {
|
||||
const {source: {links}, query} = this.props
|
||||
this.props.actions.editRawTextAsync(links.queries, query.id, text)
|
||||
handleChooseTemplate(template) {
|
||||
this.setState({value: template.query})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {query, timeRange} = this.props
|
||||
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
|
||||
const {config: {status}} = this.props
|
||||
const {value} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-builder--tab-contents">
|
||||
<div>
|
||||
<RawQueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
|
||||
{this.renderLists()}
|
||||
</div>
|
||||
<div className="query-editor">
|
||||
<textarea
|
||||
className="query-editor--field"
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleUpdate}
|
||||
ref={editor => (this.editor = editor)}
|
||||
value={value}
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
{this.renderStatus(status)}
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderStatus(status) {
|
||||
if (!status) {
|
||||
return <div className="query-editor--status" />
|
||||
}
|
||||
|
||||
renderLists() {
|
||||
const {query} = this.props
|
||||
if (status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="query-builder--columns">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
/>
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
<div
|
||||
className={classNames('query-editor--status', {
|
||||
'query-editor--error': status.error,
|
||||
'query-editor--success': status.success,
|
||||
'query-editor--warning': status.warn,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={classNames('icon', {
|
||||
stop: status.error,
|
||||
checkmark: status.success,
|
||||
'alert-triangle': status.warn,
|
||||
})}
|
||||
/>
|
||||
{status.error || status.warn || status.success}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const QueryTabItem = React.createClass({
|
||||
const QueryMakerTab = React.createClass({
|
||||
propTypes: {
|
||||
isActive: PropTypes.bool,
|
||||
query: PropTypes.shape({
|
||||
|
@ -24,12 +24,12 @@ const QueryTabItem = React.createClass({
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div className={classNames('query-builder--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
|
||||
<span className="query-builder--tab-label">{this.props.queryTabText}</span>
|
||||
<span className="query-builder--tab-delete" onClick={this.handleDelete}></span>
|
||||
<div className={classNames('query-maker--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
|
||||
<label>{this.props.queryTabText}</label>
|
||||
<span className="query-maker--delete" onClick={this.handleDelete}></span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default QueryTabItem
|
||||
export default QueryMakerTab
|
|
@ -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
|
|
@ -1,10 +1,13 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
|
||||
import Dimensions from 'react-dimensions'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
|
||||
import {Table, Column, Cell} from 'fixed-data-table'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
|
@ -105,7 +108,6 @@ const ChronoTable = React.createClass({
|
|||
isLoading: false,
|
||||
cellData: emptyCells,
|
||||
})
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
|
|
@ -91,13 +91,16 @@ const TagList = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Tags</div>
|
||||
{(!query.database || !query.measurement || !query.retentionPolicy) ? null : <div className="qeditor--list-header">
|
||||
<div className="toggle toggle-sm">
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: query.areTagsAccepted})}>=</div>
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: !query.areTagsAccepted})}>!=</div>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="query-builder--heading">
|
||||
<span>Tags</span>
|
||||
{(!query.database || !query.measurement || !query.retentionPolicy) ? null :
|
||||
<div className={cx('flip-toggle', {flipped: query.areTagsAccepted})} onClick={this.handleAcceptReject}>
|
||||
<div className="flip-toggle--container">
|
||||
<div className="flip-toggle--front">!=</div>
|
||||
<div className="flip-toggle--back">=</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
)
|
||||
|
@ -106,11 +109,15 @@ const TagList = React.createClass({
|
|||
renderList() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query
|
||||
if (!database || !measurement || !retentionPolicy) {
|
||||
return <div className="qeditor--empty">No <strong>Measurement</strong> selected</div>
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Measurement</strong> selected</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="qeditor--list">
|
||||
<div className="query-builder--list">
|
||||
{_.map(this.state.tags, (tagValues, tagKey) => {
|
||||
return (
|
||||
<TagListItem
|
||||
|
@ -124,7 +131,7 @@ const TagList = React.createClass({
|
|||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -55,23 +55,23 @@ const TagListItem = React.createClass({
|
|||
const filtered = tagValues.filter((v) => v.match(this.state.filterText))
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className="tag-value-list__filter-container">
|
||||
<input className="tag-value-list__filter" ref="filterText" placeholder={`Filter within ${this.props.tagKey}`} type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<div className="query-builder--sub-list">
|
||||
<div className="query-builder--filter">
|
||||
<input className="form-control input-sm" ref="filterText" placeholder={`Filter within ${this.props.tagKey}`} type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<span className="icon search"></span>
|
||||
</div>
|
||||
<ul className="tag-value-list">
|
||||
{filtered.map((v) => {
|
||||
const cx = classNames('tag-value-list__item qeditor--list-item', {active: selectedTagValues.indexOf(v) > -1})
|
||||
return (
|
||||
<li className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
|
||||
<div className="tag-value-list__checkbox"></div>
|
||||
<div className="tag-value-list__item-label">{v}</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
{filtered.map((v) => {
|
||||
const cx = classNames('query-builder--list-item', {active: selectedTagValues.indexOf(v) > -1})
|
||||
return (
|
||||
<div className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
|
||||
<span>
|
||||
<div className="query-builder--checkbox"></div>
|
||||
{v}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
|
@ -83,23 +83,20 @@ const TagListItem = React.createClass({
|
|||
render() {
|
||||
const {tagKey, tagValues} = this.props
|
||||
const {isOpen} = this.state
|
||||
const itemClasses = classNames("qeditor--list-item tag-list__item", {open: isOpen})
|
||||
const tagItemLabel = `${tagKey} — ${tagValues.length}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<li className={itemClasses} onClick={this.handleClickKey}>
|
||||
<div className="tag-list__title">
|
||||
<div className="tag-list__caret">
|
||||
<div className="icon caret-right"></div>
|
||||
</div>
|
||||
{tagKey}
|
||||
<span className="badge">{tagValues.length}</span>
|
||||
</div>
|
||||
<div className={classNames('query-builder--list-item', {active: isOpen})} onClick={this.handleClickKey}>
|
||||
<span>
|
||||
<div className="query-builder--caret icon caret-right"></div>
|
||||
{tagItemLabel}
|
||||
</span>
|
||||
<div
|
||||
className={classNames('btn btn-info btn-xs tag-list__group-by', {active: this.props.isUsingGroupBy})}
|
||||
className={classNames('btn btn-info btn-xs group-by-tag', {active: this.props.isUsingGroupBy})}
|
||||
onClick={this.handleGroupBy}>Group By {tagKey}
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
{isOpen ? this.renderTagValues() : null}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,26 +4,25 @@ import classNames from 'classnames'
|
|||
const VisHeader = ({views, view, onToggleView, name}) => (
|
||||
<div className="graph-heading">
|
||||
<div className="graph-actions">
|
||||
<ul className="toggle toggle-sm">
|
||||
{views.map(v => (
|
||||
<li
|
||||
key={v}
|
||||
onClick={() => onToggleView(v)}
|
||||
className={classNames("toggle-btn ", {active: view === v})}>
|
||||
{v}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{views.length
|
||||
? <ul className="toggle toggle-sm">
|
||||
{views.map(v => (
|
||||
<li
|
||||
key={v}
|
||||
onClick={() => onToggleView(v)}
|
||||
className={classNames('toggle-btn ', {active: view === v})}
|
||||
>
|
||||
{v}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
: null}
|
||||
</div>
|
||||
<div className="graph-title">{name}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, string} = PropTypes
|
||||
|
||||
VisHeader.propTypes = {
|
||||
views: arrayOf(string).isRequired,
|
||||
|
|
|
@ -49,7 +49,7 @@ const VisView = ({
|
|||
autoRefresh={autoRefresh}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={true}
|
||||
showSingleStat={cellType === "line-plus-single-stat"}
|
||||
showSingleStat={cellType === 'line-plus-single-stat'}
|
||||
displayOptions={displayOptions}
|
||||
editQueryStatus={editQueryStatus}
|
||||
/>
|
||||
|
|
|
@ -3,18 +3,9 @@ import buildInfluxQLQuery from 'utils/influxql'
|
|||
import classNames from 'classnames'
|
||||
import VisHeader from 'src/data_explorer/components/VisHeader'
|
||||
import VisView from 'src/data_explorer/components/VisView'
|
||||
import {GRAPH, TABLE} from 'src/shared/constants'
|
||||
|
||||
const GRAPH = 'graph'
|
||||
const TABLE = 'table'
|
||||
const VIEWS = [GRAPH, TABLE]
|
||||
|
||||
const {
|
||||
func,
|
||||
arrayOf,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
const Visualization = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -30,6 +21,7 @@ const Visualization = React.createClass({
|
|||
height: string,
|
||||
heightPixels: number,
|
||||
editQueryStatus: func.isRequired,
|
||||
views: arrayOf(string).isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
|
@ -49,13 +41,25 @@ const Visualization = React.createClass({
|
|||
}
|
||||
|
||||
return {
|
||||
view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH,
|
||||
view: typeof queryConfigs[activeQueryIndex].rawText === 'string'
|
||||
? TABLE
|
||||
: GRAPH,
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
cellName: '',
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {queryConfigs, activeQueryIndex} = nextProps
|
||||
if (!queryConfigs.length || activeQueryIndex === null || activeQueryIndex === this.props.activeQueryIndex) {
|
||||
if (
|
||||
!queryConfigs.length ||
|
||||
activeQueryIndex === null ||
|
||||
activeQueryIndex === this.props.activeQueryIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -71,8 +75,10 @@ const Visualization = React.createClass({
|
|||
|
||||
render() {
|
||||
const {
|
||||
views,
|
||||
height,
|
||||
cellType,
|
||||
cellName,
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
heightPixels,
|
||||
|
@ -83,18 +89,28 @@ const Visualization = React.createClass({
|
|||
const {source: {links: {proxy}}} = this.context
|
||||
const {view} = this.state
|
||||
|
||||
const statements = queryConfigs.map((query) => {
|
||||
const statements = queryConfigs.map(query => {
|
||||
const text = query.rawText || buildInfluxQLQuery(timeRange, query)
|
||||
return {text, id: query.id}
|
||||
})
|
||||
const queries = statements.filter((s) => s.text !== null).map((s) => {
|
||||
const queries = statements.filter(s => s.text !== null).map(s => {
|
||||
return {host: [proxy], text: s.text, id: s.id}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="graph" style={{height}}>
|
||||
<VisHeader views={VIEWS} view={view} onToggleView={this.handleToggleView} name={name || 'Graph'}/>
|
||||
<div className={classNames({"graph-container": view === GRAPH, "table-container": view === TABLE})}>
|
||||
<VisHeader
|
||||
views={views}
|
||||
view={view}
|
||||
onToggleView={this.handleToggleView}
|
||||
name={cellName}
|
||||
/>
|
||||
<div
|
||||
className={classNames({
|
||||
'graph-container': view === GRAPH,
|
||||
'table-container': view === TABLE,
|
||||
})}
|
||||
>
|
||||
<VisView
|
||||
view={view}
|
||||
queries={queries}
|
||||
|
|
|
@ -24,8 +24,8 @@ export const QUERY_TEMPLATES = [
|
|||
{text: 'Create Continuous Query', query: 'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END'},
|
||||
{text: 'Drop Continuous Query', query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"'},
|
||||
{text: 'Show Users', query: 'SHOW USERS'},
|
||||
{text: 'Create User', query: `CREATE USER "username" WITH PASSWORD 'password'`},
|
||||
{text: 'Create Admin User', query: `CREATE USER "username" WITH PASSWORD 'password' WITH ALL PRIVILEGES`},
|
||||
{text: 'Create User', query: 'CREATE USER "username" WITH PASSWORD \'password\''},
|
||||
{text: 'Create Admin User', query: 'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES'},
|
||||
{text: 'Drop User', query: 'DROP USER "username"'},
|
||||
{text: 'Show Stats', query: 'SHOW STATS'},
|
||||
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
|
||||
|
|
|
@ -4,21 +4,18 @@ import {bindActionCreators} from 'redux'
|
|||
|
||||
import _ from 'lodash'
|
||||
|
||||
import QueryBuilder from '../components/QueryBuilder'
|
||||
import QueryMaker from '../components/QueryMaker'
|
||||
import Visualization from '../components/Visualization'
|
||||
import Header from '../containers/Header'
|
||||
import ResizeContainer, {ResizeBottom} from 'src/shared/components/ResizeContainer'
|
||||
import ResizeContainer, {
|
||||
ResizeBottom,
|
||||
} from 'src/shared/components/ResizeContainer'
|
||||
|
||||
import {VIS_VIEWS} from 'src/shared/constants'
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
import * as viewActions from 'src/data_explorer/actions/view'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
const DataExplorer = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -94,7 +91,7 @@ const DataExplorer = React.createClass({
|
|||
timeRange={timeRange}
|
||||
/>
|
||||
<ResizeContainer>
|
||||
<QueryBuilder
|
||||
<QueryMaker
|
||||
source={source}
|
||||
queries={queryConfigs}
|
||||
actions={queryConfigActions}
|
||||
|
@ -111,6 +108,7 @@ const DataExplorer = React.createClass({
|
|||
queryConfigs={queryConfigs}
|
||||
activeQueryIndex={activeQueryIndex}
|
||||
editQueryStatus={queryConfigActions.editQueryStatus}
|
||||
views={VIS_VIEWS}
|
||||
/>
|
||||
</ResizeBottom>
|
||||
</ResizeContainer>
|
||||
|
@ -120,7 +118,12 @@ const DataExplorer = React.createClass({
|
|||
})
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {app: {persisted: {autoRefresh}}, timeRange, queryConfigs, dataExplorer} = state
|
||||
const {
|
||||
app: {persisted: {autoRefresh}},
|
||||
timeRange,
|
||||
queryConfigs,
|
||||
dataExplorer,
|
||||
} = state
|
||||
const queryConfigValues = _.values(queryConfigs)
|
||||
|
||||
return {
|
||||
|
|
|
@ -40,7 +40,7 @@ const Header = React.createClass({
|
|||
const {autoRefresh, actions: {handleChooseAutoRefresh}, timeRange} = this.props
|
||||
|
||||
return (
|
||||
<div className="page-header">
|
||||
<div className="page-header full-width-no-scrollbar">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1>Data Explorer</h1>
|
||||
|
|
|
@ -5,7 +5,7 @@ import _ from 'lodash'
|
|||
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
|
||||
return proxy({
|
||||
source: proxyLink,
|
||||
query: `select mean(usage_user) from cpu where cpu = 'cpu-total' and time > now() - 10m group by host; select mean("load1") from "system" where time > now() - 10m group by host; select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); show tag values from /win_system|system/ with key = "host"`,
|
||||
query: 'select mean(usage_user) from cpu where cpu = \'cpu-total\' and time > now() - 10m group by host; select mean("load1") from "system" where time > now() - 10m group by host; select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); show tag values from /win_system|system/ with key = "host"',
|
||||
db: telegrafDB,
|
||||
}).then((resp) => {
|
||||
const hosts = {}
|
||||
|
@ -88,6 +88,7 @@ export async function getAllHosts(proxyLink, telegrafDB) {
|
|||
return hosts
|
||||
} catch (error) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,11 +83,11 @@ const HostsTable = React.createClass({
|
|||
sortableClasses(key) {
|
||||
if (this.state.sortKey === key) {
|
||||
if (this.state.sortDirection === 'asc') {
|
||||
return "sortable-header sorting-up"
|
||||
return 'sortable-header sorting-up'
|
||||
}
|
||||
return "sortable-header sorting-down"
|
||||
return 'sortable-header sorting-down'
|
||||
}
|
||||
return "sortable-header"
|
||||
return 'sortable-header'
|
||||
},
|
||||
|
||||
render() {
|
||||
|
@ -99,11 +99,11 @@ const HostsTable = React.createClass({
|
|||
let hostsTitle
|
||||
|
||||
if (hostsLoading) {
|
||||
hostsTitle = `Loading Hosts...`
|
||||
hostsTitle = 'Loading Hosts...'
|
||||
} else if (hostsError.length) {
|
||||
hostsTitle = `There was a problem loading hosts`
|
||||
hostsTitle = 'There was a problem loading hosts'
|
||||
} else if (hosts.length === 0) {
|
||||
hostsTitle = `No hosts found`
|
||||
hostsTitle = 'No hosts found'
|
||||
} else if (hostCount === 1) {
|
||||
hostsTitle = `${hostCount} Host`
|
||||
} else {
|
||||
|
@ -164,11 +164,11 @@ const HostRow = React.createClass({
|
|||
const {host, source} = this.props
|
||||
const {name, cpu, load, apps = []} = host
|
||||
|
||||
let stateStr = ""
|
||||
let stateStr = ''
|
||||
if (host.deltaUptime < 0) {
|
||||
stateStr = "table-dot dot-critical"
|
||||
stateStr = 'table-dot dot-critical'
|
||||
} else if (host.deltaUptime > 0) {
|
||||
stateStr = "table-dot dot-success"
|
||||
stateStr = 'table-dot dot-success'
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -182,7 +182,7 @@ const HostRow = React.createClass({
|
|||
return (
|
||||
<span key={app}>
|
||||
<Link
|
||||
style={{marginLeft: "2px"}}
|
||||
style={{marginLeft: '2px'}}
|
||||
to={{pathname: `/sources/${source.id}/hosts/${name}`, query: {app}}}>
|
||||
{app}
|
||||
</Link>
|
||||
|
|
118
ui/src/index.js
118
ui/src/index.js
|
@ -1,15 +1,16 @@
|
|||
import React from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {Provider} from 'react-redux'
|
||||
import {Router, Route, Redirect, useRouterHistory} from 'react-router'
|
||||
import {Router, Route, useRouterHistory} from 'react-router'
|
||||
import {createHistory} from 'history'
|
||||
import {syncHistoryWithStore} from 'react-router-redux'
|
||||
|
||||
import App from 'src/App'
|
||||
import AlertsApp from 'src/alerts'
|
||||
import CheckSources from 'src/CheckSources'
|
||||
import {HostsPage, HostPage} from 'src/hosts'
|
||||
import {KubernetesPage} from 'src/kubernetes'
|
||||
import {Login} from 'src/auth'
|
||||
import {Login, UserIsAuthenticated, UserIsNotAuthenticated} from 'src/auth'
|
||||
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor'
|
||||
import DataExplorer from 'src/data_explorer'
|
||||
import {DashboardsPage, DashboardPage} from 'src/dashboards'
|
||||
|
@ -17,18 +18,18 @@ import {CreateSource, SourcePage, ManageSources} from 'src/sources'
|
|||
import {AdminPage} from 'src/admin'
|
||||
import NotFound from 'src/shared/components/NotFound'
|
||||
import configureStore from 'src/store/configureStore'
|
||||
import {getMe, getSources} from 'shared/apis'
|
||||
import {receiveMe} from 'shared/actions/me'
|
||||
import {receiveAuth} from 'shared/actions/auth'
|
||||
import {disablePresentationMode} from 'shared/actions/app'
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
import {loadLocalStorage} from './localStorage'
|
||||
|
||||
import {getMe} from 'shared/apis'
|
||||
|
||||
import {disablePresentationMode} from 'shared/actions/app'
|
||||
import {authRequested, authReceived, meRequested, meReceived} from 'shared/actions/auth'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import 'src/style/chronograf.scss'
|
||||
|
||||
import {HTTP_FORBIDDEN, HEARTBEAT_INTERVAL} from 'shared/constants'
|
||||
import {HEARTBEAT_INTERVAL} from 'shared/constants'
|
||||
|
||||
const store = configureStore(loadLocalStorage())
|
||||
const rootNode = document.getElementById('react-root')
|
||||
|
||||
let browserHistory
|
||||
|
@ -40,98 +41,66 @@ if (basepath) {
|
|||
})
|
||||
} else {
|
||||
browserHistory = useRouterHistory(createHistory)({
|
||||
basename: "",
|
||||
basename: '',
|
||||
})
|
||||
}
|
||||
|
||||
const store = configureStore(loadLocalStorage(), browserHistory)
|
||||
const {dispatch} = store
|
||||
|
||||
browserHistory.listen(() => {
|
||||
store.dispatch(disablePresentationMode())
|
||||
dispatch(disablePresentationMode())
|
||||
})
|
||||
|
||||
window.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
store.dispatch(disablePresentationMode())
|
||||
dispatch(disablePresentationMode())
|
||||
}
|
||||
})
|
||||
|
||||
const history = syncHistoryWithStore(browserHistory, store)
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
loggedIn: null,
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
componentWillMount() {
|
||||
this.checkAuth()
|
||||
},
|
||||
activeSource(sources) {
|
||||
const defaultSource = sources.find((s) => s.default)
|
||||
if (defaultSource && defaultSource.id) {
|
||||
return defaultSource
|
||||
|
||||
async checkAuth() {
|
||||
dispatch(authRequested())
|
||||
dispatch(meRequested())
|
||||
try {
|
||||
await this.startHeartbeat({shouldDispatchResponse: true})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
return sources[0]
|
||||
},
|
||||
|
||||
redirectFromRoot(_, replace, callback) {
|
||||
getSources().then(({data: {sources}}) => {
|
||||
if (sources && sources.length) {
|
||||
const path = `/sources/${this.activeSource(sources).id}/hosts`
|
||||
replace(path)
|
||||
}
|
||||
callback()
|
||||
})
|
||||
},
|
||||
|
||||
checkAuth() {
|
||||
if (store.getState().me.links) {
|
||||
return this.setState({loggedIn: true})
|
||||
}
|
||||
|
||||
this.heartbeat({shouldDispatchResponse: true})
|
||||
},
|
||||
|
||||
async heartbeat({shouldDispatchResponse}) {
|
||||
async startHeartbeat({shouldDispatchResponse}) {
|
||||
try {
|
||||
const {data: me, auth} = await getMe()
|
||||
if (shouldDispatchResponse) {
|
||||
store.dispatch(receiveMe(me))
|
||||
store.dispatch(receiveAuth(auth))
|
||||
this.setState({loggedIn: true})
|
||||
dispatch(authReceived(auth))
|
||||
dispatch(meReceived(me))
|
||||
}
|
||||
|
||||
setTimeout(this.heartbeat.bind(null, {shouldDispatchResponse: false}), HEARTBEAT_INTERVAL)
|
||||
setTimeout(() => {
|
||||
if (store.getState().auth.me !== null) {
|
||||
this.startHeartbeat({shouldDispatchResponse: false})
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
} catch (error) {
|
||||
if (error.auth) {
|
||||
store.dispatch(receiveAuth(error.auth))
|
||||
}
|
||||
if (error.status === HTTP_FORBIDDEN) {
|
||||
store.dispatch(publishNotification('error', 'Session timed out. Please login again.'))
|
||||
} else {
|
||||
store.dispatch(publishNotification('error', 'Cannot communicate with server.'))
|
||||
}
|
||||
|
||||
this.setState({loggedIn: false})
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.state.loggedIn === null) {
|
||||
return <div className="page-spinner"></div>
|
||||
}
|
||||
if (this.state.loggedIn === false) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/login" component={Login} />
|
||||
<Redirect from="*" to="/login" />
|
||||
</Router>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/" component={CreateSource} onEnter={this.redirectFromRoot} />
|
||||
<Route path="/sources/new" component={CreateSource} />
|
||||
<Route path="/sources/:sourceID" component={App}>
|
||||
<Router history={history}>
|
||||
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
|
||||
<Route path="login" component={UserIsNotAuthenticated(Login)} />
|
||||
<Route path="sources/new" component={UserIsAuthenticated(CreateSource)} />
|
||||
<Route path="sources/:sourceID" component={UserIsAuthenticated(App)}>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
|
@ -140,7 +109,8 @@ const Root = React.createClass({
|
|||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route path="kubernetes" component={KubernetesPage} />
|
||||
<Route path="kapacitor-config" component={KapacitorPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import uuid from 'node-uuid'
|
||||
import {getKapacitor} from 'src/shared/apis'
|
||||
import {getActiveKapacitor} from 'src/shared/apis'
|
||||
import {publishNotification} from 'src/shared/actions/notifications'
|
||||
import {
|
||||
getRules,
|
||||
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
export function fetchRule(source, ruleID) {
|
||||
return (dispatch) => {
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
getActiveKapacitor(source).then((kapacitor) => {
|
||||
getRule(kapacitor, ruleID).then(({data: rule}) => {
|
||||
dispatch({
|
||||
type: 'LOAD_RULE',
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,6 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classNames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
import DatabaseList from '../../data_explorer/components/DatabaseList'
|
||||
|
@ -8,11 +6,6 @@ import MeasurementList from '../../data_explorer/components/MeasurementList'
|
|||
import FieldList from '../../data_explorer/components/FieldList'
|
||||
import TagList from '../../data_explorer/components/TagList'
|
||||
|
||||
const DB_TAB = 'databases'
|
||||
const MEASUREMENTS_TAB = 'measurments'
|
||||
const FIELDS_TAB = 'fields'
|
||||
const TAGS_TAB = 'tags'
|
||||
|
||||
export const DataSection = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
|
@ -50,22 +43,12 @@ export const DataSection = React.createClass({
|
|||
return {source: this.props.source}
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
activeTab: DB_TAB,
|
||||
}
|
||||
},
|
||||
|
||||
handleChooseNamespace(namespace) {
|
||||
this.props.actions.chooseNamespace(this.props.query.id, namespace)
|
||||
|
||||
this.setState({activeTab: MEASUREMENTS_TAB})
|
||||
},
|
||||
|
||||
handleChooseMeasurement(measurement) {
|
||||
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
|
||||
|
||||
this.setState({activeTab: FIELDS_TAB})
|
||||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
|
@ -92,97 +75,50 @@ export const DataSection = React.createClass({
|
|||
this.props.actions.groupByTag(this.props.query.id, tagKey)
|
||||
},
|
||||
|
||||
handleClickTab(tab) {
|
||||
this.setState({activeTab: tab})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {query, timeRange: {lower}} = this.props
|
||||
const statement = query.rawText || buildInfluxQLQuery({lower}, query) || `SELECT "fields" FROM "db"."rp"."measurement"`
|
||||
const statement = query.rawText || buildInfluxQLQuery({lower}, query) || 'Build a query below'
|
||||
|
||||
return (
|
||||
<div className="kapacitor-rule-section">
|
||||
<div className="kapacitor-rule-section kapacitor-metric-selector">
|
||||
<h3 className="rule-section-heading">Select a Time Series</h3>
|
||||
<div className="rule-section-body">
|
||||
<div className="qeditor kapacitor-metric-selector">
|
||||
<div className="qeditor--query-preview">
|
||||
<pre className={classNames("", {"rq-mode": query.rawText})}><code>{statement}</code></pre>
|
||||
</div>
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
<pre><code>{statement}</code></pre>
|
||||
{this.renderQueryBuilder()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderEditor() {
|
||||
const {activeTab} = this.state
|
||||
renderQueryBuilder() {
|
||||
const {query} = this.props
|
||||
if (query.rawText) {
|
||||
return (
|
||||
<div className="generic-empty-state query-editor-empty-state">
|
||||
<p className="margin-bottom-zero">
|
||||
<span className="icon alert-triangle"></span>
|
||||
Only editable in the <strong>Raw Query</strong> tab.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="kapacitor-tab-list">
|
||||
<div className="qeditor--tabs">
|
||||
<div onClick={_.wrap(DB_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === DB_TAB})}>Databases</div>
|
||||
<div onClick={_.wrap(MEASUREMENTS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === MEASUREMENTS_TAB})}>Measurements</div>
|
||||
<div onClick={_.wrap(FIELDS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === FIELDS_TAB})}>Fields</div>
|
||||
<div onClick={_.wrap(TAGS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === TAGS_TAB})}>Tags</div>
|
||||
</div>
|
||||
{this.renderList()}
|
||||
<div className="query-builder">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
isKapacitorRule={true}
|
||||
/>
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderList() {
|
||||
const {query} = this.props
|
||||
|
||||
switch (this.state.activeTab) {
|
||||
case DB_TAB:
|
||||
return (
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
)
|
||||
case MEASUREMENTS_TAB:
|
||||
return (
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
)
|
||||
case FIELDS_TAB:
|
||||
return (
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
isKapacitorRule={true}
|
||||
/>
|
||||
)
|
||||
case TAGS_TAB:
|
||||
return (
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return <ul className="qeditor--list"></ul>
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default DataSection
|
||||
|
|
|
@ -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
|
|
@ -1,31 +1,9 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import AlertOutputs from './AlertOutputs'
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
const KapacitorForm = React.createClass({
|
||||
propTypes: {
|
||||
onSubmit: func.isRequired,
|
||||
onInputChange: func.isRequired,
|
||||
onReset: func.isRequired,
|
||||
kapacitor: shape({
|
||||
url: string.isRequired,
|
||||
name: string.isRequired,
|
||||
username: string,
|
||||
password: string,
|
||||
}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
exists: bool.isRequired,
|
||||
},
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import AlertTabs from './AlertTabs'
|
||||
|
||||
class KapacitorForm extends Component {
|
||||
render() {
|
||||
const {onInputChange, onReset, kapacitor, source, onSubmit} = this.props
|
||||
const {onInputChange, onReset, kapacitor, onSubmit} = this.props
|
||||
const {url, name, username, password} = kapacitor
|
||||
|
||||
return (
|
||||
|
@ -42,21 +20,15 @@ const KapacitorForm = React.createClass({
|
|||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="col-md-3">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Connection Details</h2>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<p className="no-user-select">
|
||||
Kapacitor is used as the monitoring and alerting agent.
|
||||
This page will let you configure which Kapacitor to use and
|
||||
set up alert end points like email, Slack, and others.
|
||||
</p>
|
||||
<hr/>
|
||||
<h4 className="text-center no-user-select">Connect Kapacitor to Source</h4>
|
||||
<h4 className="text-center">{source.url}</h4>
|
||||
<br/>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div>
|
||||
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="url">Kapacitor URL</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -64,10 +36,11 @@ const KapacitorForm = React.createClass({
|
|||
name="url"
|
||||
placeholder={url}
|
||||
value={url}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-0">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -75,10 +48,11 @@ const KapacitorForm = React.createClass({
|
|||
name="name"
|
||||
placeholder={name}
|
||||
value={name}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-4 col-sm-offset-2 col-md-4 col-md-offset-2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -86,10 +60,11 @@ const KapacitorForm = React.createClass({
|
|||
name="username"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-4 col-md-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -99,21 +74,20 @@ const KapacitorForm = React.createClass({
|
|||
placeholder="password"
|
||||
value={password}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-info" type="button" onClick={onReset}>Reset to Default</button>
|
||||
<button className="btn btn-success" type="submit">Connect Kapacitor</button>
|
||||
<button className="btn btn-info" type="button" onClick={onReset}>Reset</button>
|
||||
<button className="btn btn-success" type="submit">Connect</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="col-md-9">
|
||||
{this.renderAlertOutputs()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -121,26 +95,50 @@ const KapacitorForm = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: move these to another page. they dont belong on this page
|
||||
renderAlertOutputs() {
|
||||
const {exists, kapacitor, addFlashMessage, source} = this.props
|
||||
|
||||
if (exists) {
|
||||
return <AlertOutputs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
|
||||
return <AlertTabs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Configure Alert Endpoints</h2>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<h4 className="text-center">Configure Alert Endpoints</h4>
|
||||
<br/>
|
||||
<p className="text-center">Set your Kapacitor connection info to configure alerting endpoints.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
KapacitorForm.propTypes = {
|
||||
onSubmit: func.isRequired,
|
||||
onInputChange: func.isRequired,
|
||||
onReset: func.isRequired,
|
||||
kapacitor: shape({
|
||||
url: string.isRequired,
|
||||
name: string.isRequired,
|
||||
username: string,
|
||||
password: string,
|
||||
}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
exists: bool.isRequired,
|
||||
}
|
||||
|
||||
export default KapacitorForm
|
||||
|
|
|
@ -87,9 +87,9 @@ export const KapacitorRule = React.createClass({
|
|||
|
||||
createRule(kapacitor, newRule).then(() => {
|
||||
router.push(`/sources/${source.id}/alert-rules`)
|
||||
addFlashMessage({type: 'success', text: `Rule successfully created`})
|
||||
addFlashMessage({type: 'success', text: 'Rule successfully created'})
|
||||
}).catch(() => {
|
||||
addFlashMessage({type: 'error', text: `There was a problem creating the rule`})
|
||||
addFlashMessage({type: 'error', text: 'There was a problem creating the rule'})
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -101,9 +101,9 @@ export const KapacitorRule = React.createClass({
|
|||
})
|
||||
|
||||
editRule(updatedRule).then(() => {
|
||||
addFlashMessage({type: 'success', text: `Rule successfully updated!`})
|
||||
addFlashMessage({type: 'success', text: 'Rule successfully updated!'})
|
||||
}).catch(() => {
|
||||
addFlashMessage({type: 'error', text: `There was a problem updating the rule`})
|
||||
addFlashMessage({type: 'error', text: 'There was a problem updating the rule'})
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => {
|
|||
id={`kapacitor-enabled ${rule.id}`}
|
||||
className="form-control-static"
|
||||
type="checkbox"
|
||||
defaultChecked={rule.status === "enabled"}
|
||||
defaultChecked={rule.status === 'enabled'}
|
||||
onClick={() => onChangeRuleStatus(rule)}
|
||||
/>
|
||||
<label htmlFor={`kapacitor-enabled ${rule.id}`}></label>
|
||||
|
|
|
@ -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
|
|
@ -29,7 +29,7 @@ export const RuleGraph = React.createClass({
|
|||
const autoRefreshMs = 30000
|
||||
const queryText = buildInfluxQLQuery({lower}, query)
|
||||
const queries = [{host: source.links.proxy, text: queryText}]
|
||||
const kapacitorLineColors = ["#4ED8A0"]
|
||||
const kapacitorLineColors = ['#4ED8A0']
|
||||
|
||||
if (!queryText) {
|
||||
return (
|
||||
|
|
|
@ -22,11 +22,13 @@ const RuleMessageAlertConfig = ({
|
|||
<p>{DEFAULT_ALERT_LABELS[alert]}</p>
|
||||
<input
|
||||
id="alert-input"
|
||||
className="form-control size-486"
|
||||
className="form-control size-486 form-control--green input-sm"
|
||||
type="text"
|
||||
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
|
||||
onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)}
|
||||
value={ALERT_NODES_ACCESSORS[alert](rule)}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -101,9 +101,9 @@ const Threshold = React.createClass({
|
|||
<span>{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}</span>
|
||||
<p>is</p>
|
||||
<Dropdown className="size-176 dropdown-kapacitor" items={operators} selected={operator} onChoose={this.handleDropdownChange} />
|
||||
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange} />
|
||||
<input className="form-control input-sm size-166 form-control--green" type="text" spellCheck="false" ref={(r) => this.valueInput = r} defaultValue={value} onKeyUp={this.handleInputChange} />
|
||||
{ (operator === 'inside range' || operator === 'outside range') &&
|
||||
<input className="form-control input-sm size-166 form-control--green" type="text" ref={(r) => this.valueRangeInput = r} defaultValue={rangeValue} onKeyUp={this.handleInputChange} />
|
||||
<input className="form-control input-sm size-166 form-control--green" type="text" spellCheck="false" ref={(r) => this.valueRangeInput = r} defaultValue={rangeValue} onKeyUp={this.handleInputChange} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
@ -153,6 +153,7 @@ const Relative = React.createClass({
|
|||
onKeyUp={this.handleInputChange}
|
||||
required={true}
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<p>{ change === CHANGES[1] ? '%' : '' }</p>
|
||||
</div>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -64,25 +64,20 @@ const OpsGenieConfig = React.createClass({
|
|||
const {currentTeams, currentRecipients} = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">OpsGenie Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Have alerts sent to OpsGenie.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
|
||||
</div>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
|
||||
</div>
|
||||
|
||||
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
|
||||
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
|
||||
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
|
||||
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -29,14 +29,14 @@ export const ALERTS = ['alerta', 'hipchat', 'opsgenie', 'pagerduty', 'sensu', 's
|
|||
export const DEFAULT_RULE_ID = 'DEFAULT_RULE_ID'
|
||||
|
||||
export const RULE_MESSAGE_TEMPLATES = {
|
||||
id: {label: "{{.ID}}", text: "The ID of the alert"},
|
||||
name: {label: "{{.Name}}", text: "Measurement name"},
|
||||
taskName: {label: "{{.TaskName}}", text: "The name of the task"},
|
||||
group: {label: "{{.Group}}", text: "Concatenation of all group-by tags of the form <code>[key=value,]+</code>. If no groupBy is performed equal to literal "nil""},
|
||||
tags: {label: "{{.Tags}}", text: "Map of tags. Use <code>{{ index .Tags "key" }}</code> to get a specific tag value"},
|
||||
level: {label: "{{.Level}}", text: "Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>"},
|
||||
fields: {label: `{{ index .Fields "value" }}`, text: "Map of fields. Use <code>{{ index .Fields "key" }}</code> to get a specific field value"},
|
||||
time: {label: "{{.Time}}", text: "The time of the point that triggered the event"},
|
||||
id: {label: '{{.ID}}', text: 'The ID of the alert'},
|
||||
name: {label: '{{.Name}}', text: 'Measurement name'},
|
||||
taskName: {label: '{{.TaskName}}', text: 'The name of the task'},
|
||||
group: {label: '{{.Group}}', text: 'Concatenation of all group-by tags of the form <code>[key=value,]+</code>. If no groupBy is performed equal to literal "nil"'},
|
||||
tags: {label: '{{.Tags}}', text: 'Map of tags. Use <code>{{ index .Tags "key" }}</code> to get a specific tag value'},
|
||||
level: {label: '{{.Level}}', text: 'Alert Level, one of: <code>INFO</code><code>WARNING</code><code>CRITICAL</code>'},
|
||||
fields: {label: '{{ index .Fields "value" }}', text: 'Map of fields. Use <code>{{ index .Fields "key" }}</code> to get a specific field value'},
|
||||
time: {label: '{{.Time}}', text: 'The time of the point that triggered the event'},
|
||||
}
|
||||
|
||||
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec']
|
||||
|
@ -51,8 +51,8 @@ export const DEFAULT_ALERT_LABELS = {
|
|||
alerta: 'Paste Alerta TICKscript:',
|
||||
}
|
||||
export const DEFAULT_ALERT_PLACEHOLDERS = {
|
||||
http: 'http://',
|
||||
tcp: 'Address:',
|
||||
http: 'Ex: http://example.com/api/alert',
|
||||
tcp: 'Ex: exampleendpoint.com:5678',
|
||||
exec: 'Ex: woogie boogie',
|
||||
smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
|
||||
slack: '#alerts',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {
|
||||
getKapacitor,
|
||||
createKapacitor,
|
||||
|
@ -7,26 +7,13 @@ import {
|
|||
} from 'shared/apis'
|
||||
import KapacitorForm from '../components/KapacitorForm'
|
||||
|
||||
const defaultName = "My Kapacitor"
|
||||
const kapacitorPort = "9092"
|
||||
const defaultName = 'My Kapacitor'
|
||||
const kapacitorPort = '9092'
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
export const KapacitorPage = React.createClass({
|
||||
propTypes: {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
url: string.isRequired,
|
||||
}),
|
||||
addFlashMessage: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
class KapacitorPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
kapacitor: {
|
||||
url: this._parseKapacitorURL(),
|
||||
name: defaultName,
|
||||
|
@ -35,39 +22,27 @@ export const KapacitorPage = React.createClass({
|
|||
},
|
||||
exists: false,
|
||||
}
|
||||
},
|
||||
|
||||
this.handleInputChange = ::this.handleInputChange
|
||||
this.handleSubmit = ::this.handleSubmit
|
||||
this.handleResetToDefaults = ::this.handleResetToDefaults
|
||||
this._parseKapacitorURL = ::this._parseKapacitorURL
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {source} = this.props
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
if (!kapacitor) {
|
||||
return
|
||||
}
|
||||
const {source, params: {id}} = this.props
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
getKapacitor(source, id).then((kapacitor) => {
|
||||
this.setState({kapacitor, exists: true}, () => {
|
||||
pingKapacitor(kapacitor).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: 'Could not connect to Kapacitor. Check settings.'})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
const {kapacitor, exists} = this.state
|
||||
|
||||
return (
|
||||
<KapacitorForm
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
onReset={this.handleResetToDefaults}
|
||||
kapacitor={kapacitor}
|
||||
source={source}
|
||||
addFlashMessage={addFlashMessage}
|
||||
exists={exists}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const {value, name} = e.target
|
||||
|
@ -76,8 +51,7 @@ export const KapacitorPage = React.createClass({
|
|||
const update = {[name]: value.trim()}
|
||||
return {kapacitor: {...prevState.kapacitor, ...update}}
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
|
@ -99,7 +73,7 @@ export const KapacitorPage = React.createClass({
|
|||
addFlashMessage({type: 'error', text: 'There was a problem creating the Kapacitor record'})
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleResetToDefaults(e) {
|
||||
e.preventDefault()
|
||||
|
@ -111,14 +85,48 @@ export const KapacitorPage = React.createClass({
|
|||
}
|
||||
|
||||
this.setState({kapacitor: {...defaultState}})
|
||||
},
|
||||
}
|
||||
|
||||
_parseKapacitorURL() {
|
||||
const parser = document.createElement('a')
|
||||
parser.href = this.props.source.url
|
||||
|
||||
return `${parser.protocol}//${parser.hostname}:${kapacitorPort}`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
const {kapacitor, exists} = this.state
|
||||
|
||||
return (
|
||||
<KapacitorForm
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
onReset={this.handleResetToDefaults}
|
||||
kapacitor={kapacitor}
|
||||
source={source}
|
||||
addFlashMessage={addFlashMessage}
|
||||
exists={exists}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
KapacitorPage.propTypes = {
|
||||
addFlashMessage: func,
|
||||
params: shape({
|
||||
id: string,
|
||||
}).isRequired,
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
url: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default KapacitorPage
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import * as kapacitorActionCreators from '../actions/view'
|
||||
import * as queryActionCreators from '../../data_explorer/actions/view'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {getKapacitor, getKapacitorConfig} from 'shared/apis/index'
|
||||
import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index'
|
||||
import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'
|
||||
import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
|
||||
|
||||
|
@ -53,7 +52,7 @@ export const KapacitorRulePage = React.createClass({
|
|||
kapacitorActions.loadDefaultRule()
|
||||
}
|
||||
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
getActiveKapacitor(source).then((kapacitor) => {
|
||||
this.setState({kapacitor})
|
||||
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
|
||||
const enabledAlerts = Object.keys(sections).filter((section) => {
|
||||
|
@ -61,9 +60,9 @@ export const KapacitorRulePage = React.createClass({
|
|||
})
|
||||
this.setState({enabledAlerts})
|
||||
}).catch(() => {
|
||||
addFlashMessage({type: 'error', text: `There was a problem communicating with Kapacitor`})
|
||||
addFlashMessage({type: 'error', text: 'There was a problem communicating with Kapacitor'})
|
||||
}).catch(() => {
|
||||
addFlashMessage({type: 'error', text: `We couldn't find a configured Kapacitor for this source`})
|
||||
addFlashMessage({type: 'error', text: 'We couldn\'t find a configured Kapacitor for this source'})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -117,4 +116,4 @@ function mapDispatchToProps(dispatch) {
|
|||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {getKapacitor} from 'src/shared/apis'
|
||||
import {getActiveKapacitor} from 'src/shared/apis'
|
||||
import * as kapacitorActionCreators from '../actions/view'
|
||||
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
|
||||
|
||||
|
@ -18,7 +18,7 @@ class KapacitorRulesPage extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
getKapacitor(this.props.source).then((kapacitor) => {
|
||||
getActiveKapacitor(this.props.source).then((kapacitor) => {
|
||||
if (kapacitor) {
|
||||
this.props.actions.fetchRules(kapacitor)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,28 @@
|
|||
export function receiveAuth(auth) {
|
||||
return {
|
||||
type: 'AUTH_RECEIVED',
|
||||
payload: {
|
||||
auth,
|
||||
},
|
||||
}
|
||||
}
|
||||
export const authExpired = (auth) => ({
|
||||
type: 'AUTH_EXPIRED',
|
||||
payload: {
|
||||
auth,
|
||||
},
|
||||
})
|
||||
|
||||
export const authRequested = () => ({
|
||||
type: 'AUTH_REQUESTED',
|
||||
})
|
||||
|
||||
export const authReceived = (auth) => ({
|
||||
type: 'AUTH_RECEIVED',
|
||||
payload: {
|
||||
auth,
|
||||
},
|
||||
})
|
||||
|
||||
export const meRequested = () => ({
|
||||
type: 'ME_REQUESTED',
|
||||
})
|
||||
|
||||
export const meReceived = (me) => ({
|
||||
type: 'ME_RECEIVED',
|
||||
payload: {
|
||||
me,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export const errorThrown = (error, altText) => ({
|
||||
type: 'ERROR_THROWN',
|
||||
error,
|
||||
altText,
|
||||
})
|
|
@ -1,14 +0,0 @@
|
|||
export function receiveMe(me) {
|
||||
return {
|
||||
type: 'ME_RECEIVED',
|
||||
payload: {
|
||||
me,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return {
|
||||
type: 'LOGOUT',
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ export function publishNotification(type, message) {
|
|||
// this validator is purely for development purposes. It might make sense to move this to a middleware.
|
||||
const validTypes = ['error', 'success', 'warning']
|
||||
if (!validTypes.includes(type) || message === undefined) {
|
||||
console.error("handleNotification must have a valid type and text") // eslint-disable-line no-console
|
||||
console.error('handleNotification must have a valid type and text') // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import {deleteSource, getSources} from 'src/shared/apis'
|
||||
import {deleteSource,
|
||||
getSources,
|
||||
getKapacitors as getKapacitorsAJAX,
|
||||
updateKapacitor as updateKapacitorAJAX,
|
||||
} from 'src/shared/apis'
|
||||
import {publishNotification} from './notifications'
|
||||
|
||||
export const loadSources = (sources) => ({
|
||||
|
@ -22,6 +26,21 @@ export const addSource = (source) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const fetchKapacitors = (source, kapacitors) => ({
|
||||
type: 'LOAD_KAPACITORS',
|
||||
payload: {
|
||||
source,
|
||||
kapacitors,
|
||||
},
|
||||
})
|
||||
|
||||
export const setActiveKapacitor = (kapacitor) => ({
|
||||
type: 'SET_ACTIVE_KAPACITOR',
|
||||
payload: {
|
||||
kapacitor,
|
||||
},
|
||||
})
|
||||
|
||||
// Async action creators
|
||||
|
||||
export const removeAndLoadSources = (source) => async (dispatch) => {
|
||||
|
@ -39,6 +58,22 @@ export const removeAndLoadSources = (source) => async (dispatch) => {
|
|||
const {data: {sources: newSources}} = await getSources()
|
||||
dispatch(loadSources(newSources))
|
||||
} catch (err) {
|
||||
dispatch(publishNotification("error", "Internal Server Error. Check API Logs"))
|
||||
dispatch(publishNotification('error', 'Internal Server Error. Check API Logs'))
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchKapacitorsAsync = (source) => async (dispatch) => {
|
||||
try {
|
||||
const {data} = await getKapacitorsAJAX(source)
|
||||
dispatch(fetchKapacitors(source, data.kapacitors))
|
||||
} catch (err) {
|
||||
dispatch(publishNotification('error', `Internal Server Error. Could not retrieve kapacitors for source ${source.id}.`))
|
||||
}
|
||||
}
|
||||
|
||||
export const setActiveKapacitorAsync = (kapacitor) => async (dispatch) => {
|
||||
// eagerly update the redux state
|
||||
dispatch(setActiveKapacitor(kapacitor))
|
||||
const kapacitorPost = {...kapacitor, active: true}
|
||||
await updateKapacitorAJAX(kapacitorPost)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import {proxy} from 'utils/queryUrlGenerator'
|
|||
import {noop} from 'shared/actions/app'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
export const handleLoading = (query, editQueryStatus) => {
|
||||
editQueryStatus(query.id, {loading: true})
|
||||
}
|
||||
|
@ -41,7 +43,7 @@ export const fetchTimeSeriesAsync = async ({source, db, rp, query, templates}, e
|
|||
const {data} = await proxy({source, db, rp, query: query.text, templates})
|
||||
return handleSuccess(data, query, editQueryStatus)
|
||||
} catch (error) {
|
||||
errorThrown(error)
|
||||
handleError(error, query, editQueryStatus)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import AJAX from 'utils/ajax'
|
|||
|
||||
export function fetchLayouts() {
|
||||
return AJAX({
|
||||
url: `/chronograf/v1/layouts`,
|
||||
url: '/chronograf/v1/layouts',
|
||||
method: 'GET',
|
||||
resource: 'layouts',
|
||||
})
|
||||
|
@ -58,15 +58,37 @@ export function pingKapacitor(kapacitor) {
|
|||
})
|
||||
}
|
||||
|
||||
export function getKapacitor(source) {
|
||||
export function getKapacitor(source, kapacitorID) {
|
||||
return AJAX({
|
||||
url: `${source.links.kapacitors}/${kapacitorID}`,
|
||||
method: 'GET',
|
||||
}).then(({data}) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
export function getActiveKapacitor(source) {
|
||||
return AJAX({
|
||||
url: source.links.kapacitors,
|
||||
method: 'GET',
|
||||
}).then(({data}) => {
|
||||
return data.kapacitors[0]
|
||||
const activeKapacitor = data.kapacitors.find((k) => k.active)
|
||||
return activeKapacitor || data.kapacitors[0]
|
||||
})
|
||||
}
|
||||
|
||||
export const getKapacitors = async (source) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url: source.links.kapacitors,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function createKapacitor(source, {url, name = 'My Kapacitor', username, password}) {
|
||||
return AJAX({
|
||||
url: source.links.kapacitors,
|
||||
|
@ -80,7 +102,7 @@ export function createKapacitor(source, {url, name = 'My Kapacitor', username, p
|
|||
})
|
||||
}
|
||||
|
||||
export function updateKapacitor({links, url, name = 'My Kapacitor', username, password}) {
|
||||
export function updateKapacitor({links, url, name = 'My Kapacitor', username, password, active}) {
|
||||
return AJAX({
|
||||
url: links.self,
|
||||
method: 'PATCH',
|
||||
|
@ -89,6 +111,7 @@ export function updateKapacitor({links, url, name = 'My Kapacitor', username, pa
|
|||
url,
|
||||
username,
|
||||
password,
|
||||
active,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import _ from 'lodash'
|
|||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
export const showDatabases = async (source) => {
|
||||
const query = `SHOW DATABASES`
|
||||
const query = 'SHOW DATABASES'
|
||||
return await proxy({source, query})
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ export function showTagValues({source, database, retentionPolicy, measurement, t
|
|||
|
||||
export function showShards() {
|
||||
return AJAX({
|
||||
url: `/api/int/v1/show-shards`,
|
||||
url: '/api/int/v1/show-shards',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -13,30 +13,36 @@ const {
|
|||
string,
|
||||
} = PropTypes
|
||||
|
||||
const AutoRefresh = (ComposedComponent) => {
|
||||
const AutoRefresh = ComposedComponent => {
|
||||
const wrapper = React.createClass({
|
||||
propTypes: {
|
||||
children: element,
|
||||
autoRefresh: number.isRequired,
|
||||
templates: arrayOf(shape({
|
||||
type: string.isRequired,
|
||||
label: string.isRequired,
|
||||
tempVar: string.isRequired,
|
||||
query: shape({
|
||||
db: string.isRequired,
|
||||
rp: string,
|
||||
influxql: string.isRequired,
|
||||
}),
|
||||
values: arrayOf(shape({
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
})).isRequired,
|
||||
})),
|
||||
queries: arrayOf(shape({
|
||||
host: oneOfType([string, arrayOf(string)]),
|
||||
text: string,
|
||||
}).isRequired).isRequired,
|
||||
label: string.isRequired,
|
||||
tempVar: string.isRequired,
|
||||
query: shape({
|
||||
db: string.isRequired,
|
||||
rp: string,
|
||||
influxql: string.isRequired,
|
||||
}),
|
||||
values: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
})
|
||||
).isRequired,
|
||||
})
|
||||
),
|
||||
queries: arrayOf(
|
||||
shape({
|
||||
host: oneOfType([string, arrayOf(string)]),
|
||||
text: string,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
editQueryStatus: func,
|
||||
},
|
||||
|
||||
|
@ -51,30 +57,42 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
const {queries, autoRefresh} = this.props
|
||||
this.executeQueries(queries)
|
||||
if (autoRefresh) {
|
||||
this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh)
|
||||
this.intervalID = setInterval(
|
||||
() => this.executeQueries(queries),
|
||||
autoRefresh
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const shouldRefetch = this.queryDifference(this.props.queries, nextProps.queries).length
|
||||
const shouldRefetch = this.queryDifference(
|
||||
this.props.queries,
|
||||
nextProps.queries
|
||||
).length
|
||||
|
||||
if (shouldRefetch) {
|
||||
this.executeQueries(nextProps.queries)
|
||||
}
|
||||
|
||||
if ((this.props.autoRefresh !== nextProps.autoRefresh) || shouldRefetch) {
|
||||
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
|
||||
clearInterval(this.intervalID)
|
||||
|
||||
if (nextProps.autoRefresh) {
|
||||
this.intervalID = setInterval(() => this.executeQueries(nextProps.queries), nextProps.autoRefresh)
|
||||
this.intervalID = setInterval(
|
||||
() => this.executeQueries(nextProps.queries),
|
||||
nextProps.autoRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
queryDifference(left, right) {
|
||||
const leftStrs = left.map((q) => `${q.host}${q.text}`)
|
||||
const rightStrs = right.map((q) => `${q.host}${q.text}`)
|
||||
return _.difference(_.union(leftStrs, rightStrs), _.intersection(leftStrs, rightStrs))
|
||||
const leftStrs = left.map(q => `${q.host}${q.text}`)
|
||||
const rightStrs = right.map(q => `${q.host}${q.text}`)
|
||||
return _.difference(
|
||||
_.union(leftStrs, rightStrs),
|
||||
_.intersection(leftStrs, rightStrs)
|
||||
)
|
||||
},
|
||||
|
||||
async executeQueries(queries) {
|
||||
|
@ -87,13 +105,16 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
|
||||
this.setState({isFetching: true})
|
||||
|
||||
const timeSeriesPromises = queries.map((query) => {
|
||||
const timeSeriesPromises = queries.map(query => {
|
||||
const {host, database, rp} = query
|
||||
return fetchTimeSeriesAsync({source: host, db: database, rp, query, templates}, editQueryStatus)
|
||||
return fetchTimeSeriesAsync(
|
||||
{source: host, db: database, rp, query, templates},
|
||||
editQueryStatus
|
||||
)
|
||||
})
|
||||
|
||||
Promise.all(timeSeriesPromises).then(timeSeries => {
|
||||
const newSeries = timeSeries.map((response) => ({response}))
|
||||
const newSeries = timeSeries.map(response => ({response}))
|
||||
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
|
||||
|
||||
this.setState({
|
||||
|
@ -116,16 +137,14 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
return this.renderFetching(timeSeries)
|
||||
}
|
||||
|
||||
if (this._noResultsForQuery(timeSeries) || !this.state.lastQuerySuccessful) {
|
||||
if (
|
||||
this._noResultsForQuery(timeSeries) ||
|
||||
!this.state.lastQuerySuccessful
|
||||
) {
|
||||
return this.renderNoResults()
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...this.props}
|
||||
data={timeSeries}
|
||||
/>
|
||||
)
|
||||
return <ComposedComponent {...this.props} data={timeSeries} />
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -161,8 +180,8 @@ const AutoRefresh = (ComposedComponent) => {
|
|||
return true
|
||||
}
|
||||
|
||||
return data.every((datum) => {
|
||||
return datum.response.results.every((result) => {
|
||||
return data.every(datum => {
|
||||
return datum.response.results.every(result => {
|
||||
return Object.keys(result).length === 0
|
||||
})
|
||||
})
|
||||
|
|
|
@ -47,13 +47,13 @@ const AutoRefreshDropdown = React.createClass({
|
|||
const {milliseconds, inputValue} = this.findAutoRefreshItem(selected)
|
||||
|
||||
return (
|
||||
<div className="dropdown time-range-dropdown">
|
||||
<div className="dropdown dropdown-160">
|
||||
<div className="btn btn-sm btn-info dropdown-toggle" onClick={() => self.toggleMenu()}>
|
||||
<span className={classnames("icon", +milliseconds > 0 ? "refresh" : "pause")}></span>
|
||||
<span className={classnames('icon', +milliseconds > 0 ? 'refresh' : 'pause')}></span>
|
||||
<span className="selected-time-range">{inputValue}</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
<ul className={classnames("dropdown-menu", {show: isOpen})}>
|
||||
<ul className={classnames('dropdown-menu', {show: isOpen})}>
|
||||
<li className="dropdown-header">AutoRefresh Interval</li>
|
||||
{autoRefreshItems.map((item) => {
|
||||
return (
|
||||
|
|
|
@ -45,7 +45,7 @@ class CustomTimeRange extends Component {
|
|||
const {isVisible, onToggle, timeRange: {upper, lower}} = this.props
|
||||
|
||||
return (
|
||||
<div className={classNames("custom-time-range", {show: isVisible})} style={{display: 'flex'}}>
|
||||
<div className={classNames('custom-time-range', {show: isVisible})} style={{display: 'flex'}}>
|
||||
<button className="btn btn-sm btn-info custom-time-range--btn" onClick={onToggle}>
|
||||
<span className="icon clock"></span>
|
||||
{`${moment(lower).format('MMM Do HH:mm')} — ${moment(upper).format('MMM Do HH:mm')}`}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue