diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8bb08b6..d7d01bbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ 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 router to use auth and force /login route when auth expired 1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication + 1. [#1302](https://github.com/influxdata/chronograf/pull/1302): Add support for multiple kapacitors per source ### UI Improvements diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index 098062232..eab0a13bb 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -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 } diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index 30cdaec76..2f061886b 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -103,6 +103,7 @@ 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{} } @@ -225,47 +226,47 @@ func init() { func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } var fileDescriptorInternal = []byte{ - // 660 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xdd, 0x6e, 0xd3, 0x4a, - 0x10, 0xd6, 0xc6, 0x76, 0x7e, 0xa6, 0x3d, 0x3d, 0x47, 0xab, 0x23, 0x58, 0x71, 0x15, 0x59, 0x20, - 0x05, 0x24, 0x7a, 0x41, 0x9f, 0xa0, 0xad, 0x25, 0x14, 0x68, 0x4b, 0xd9, 0xb4, 0x70, 0x05, 0xd2, - 0x36, 0x9d, 0x34, 0x16, 0x8e, 0x6d, 0xd6, 0x36, 0xa9, 0x5f, 0x01, 0xf1, 0x0c, 0x3c, 0x00, 0x97, - 0xbc, 0x0a, 0x2f, 0x84, 0x66, 0x77, 0xed, 0xb8, 0xa2, 0xa0, 0x5e, 0x71, 0x37, 0xdf, 0xcc, 0x66, - 0x7e, 0xbe, 0xef, 0x73, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19, - 0x1f, 0x36, 0x38, 0xfc, 0xdc, 0x83, 0xfe, 0x2c, 0xab, 0xf4, 0x1c, 0xf9, 0x0e, 0xf4, 0xa6, 0x91, - 0x60, 0x63, 0x36, 0xf1, 0x64, 0x6f, 0x1a, 0x71, 0x0e, 0xfe, 0x89, 0x5a, 0xa1, 0xe8, 0x8d, 0xd9, - 0x64, 0x24, 0x4d, 0x4c, 0xb9, 0xb3, 0x3a, 0x47, 0xe1, 0xd9, 0x1c, 0xc5, 0xfc, 0x01, 0x0c, 0xcf, - 0x0b, 0xea, 0xb6, 0x42, 0xe1, 0x9b, 0x7c, 0x8b, 0xa9, 0x76, 0xaa, 0x8a, 0x62, 0x9d, 0xe9, 0x4b, - 0x11, 0xd8, 0x5a, 0x83, 0xf9, 0x7f, 0xe0, 0x9d, 0xcb, 0x23, 0xd1, 0x37, 0x69, 0x0a, 0xb9, 0x80, - 0x41, 0x84, 0x0b, 0x55, 0x25, 0xa5, 0x18, 0x8c, 0xd9, 0x64, 0x28, 0x1b, 0x48, 0x7d, 0xce, 0x30, - 0xc1, 0x2b, 0xad, 0x16, 0x62, 0x68, 0xfb, 0x34, 0x98, 0xef, 0x02, 0x9f, 0xa6, 0x05, 0xce, 0x2b, - 0x8d, 0xb3, 0x0f, 0x71, 0xfe, 0x06, 0x75, 0xbc, 0xa8, 0xc5, 0xc8, 0x34, 0xb8, 0xa5, 0x42, 0x53, - 0x8e, 0xb1, 0x54, 0x34, 0x1b, 0x4c, 0xab, 0x06, 0x86, 0xef, 0x61, 0x14, 0xa9, 0x62, 0x79, 0x91, - 0x29, 0x7d, 0x79, 0x27, 0x3a, 0x9e, 0x42, 0x30, 0xc7, 0x24, 0x29, 0x84, 0x37, 0xf6, 0x26, 0x5b, - 0xcf, 0xee, 0xef, 0xb6, 0x3c, 0xb7, 0x7d, 0x0e, 0x31, 0x49, 0xa4, 0x7d, 0x15, 0x7e, 0x63, 0xf0, - 0xcf, 0x8d, 0x02, 0xdf, 0x06, 0x76, 0x6d, 0x66, 0x04, 0x92, 0x5d, 0x13, 0xaa, 0x4d, 0xff, 0x40, - 0xb2, 0x9a, 0xd0, 0xda, 0x10, 0x1d, 0x48, 0xb6, 0x26, 0xb4, 0x34, 0xf4, 0x06, 0x92, 0x2d, 0xf9, - 0x63, 0x18, 0x7c, 0xac, 0x50, 0xc7, 0x58, 0x88, 0xc0, 0x8c, 0xfe, 0x77, 0x33, 0xfa, 0x75, 0x85, - 0xba, 0x96, 0x4d, 0x9d, 0xf6, 0x36, 0xd2, 0x58, 0x9e, 0x4d, 0x4c, 0xb9, 0x92, 0x64, 0x1c, 0xd8, - 0x1c, 0xc5, 0xee, 0x5e, 0x4b, 0x6e, 0x6f, 0x1a, 0x85, 0x5f, 0x18, 0xf4, 0x67, 0xa8, 0x3f, 0xa1, - 0xbe, 0x13, 0x15, 0x5d, 0x17, 0x78, 0x7f, 0x70, 0x81, 0x7f, 0xbb, 0x0b, 0x82, 0x8d, 0x0b, 0xfe, - 0x87, 0x60, 0xa6, 0xe7, 0xd3, 0xc8, 0x6c, 0xec, 0x49, 0x0b, 0xc2, 0xaf, 0x0c, 0xfa, 0x47, 0xaa, - 0xce, 0xaa, 0xb2, 0xb3, 0x8e, 0xd9, 0x94, 0x8f, 0x61, 0x6b, 0x3f, 0xcf, 0x93, 0x78, 0xae, 0xca, - 0x38, 0x4b, 0xdd, 0x56, 0xdd, 0x14, 0xbd, 0x38, 0x46, 0x55, 0x54, 0x1a, 0x57, 0x98, 0x96, 0x6e, - 0xbf, 0x6e, 0x8a, 0x3f, 0x84, 0xe0, 0xd0, 0x28, 0xe9, 0x1b, 0x3a, 0x77, 0x36, 0x74, 0x5a, 0x01, - 0x4d, 0x91, 0x0e, 0xd9, 0xaf, 0xca, 0x6c, 0x91, 0x64, 0x6b, 0xb3, 0xf1, 0x50, 0xb6, 0x38, 0xfc, - 0xc1, 0xc0, 0xff, 0x5b, 0x9a, 0x6e, 0x03, 0x8b, 0x9d, 0xa0, 0x2c, 0x6e, 0x15, 0x1e, 0x74, 0x14, - 0x16, 0x30, 0xa8, 0xb5, 0x4a, 0xaf, 0xb0, 0x10, 0xc3, 0xb1, 0x37, 0xf1, 0x64, 0x03, 0x4d, 0x25, - 0x51, 0x17, 0x98, 0x14, 0x62, 0x34, 0xf6, 0xc8, 0xfe, 0x0e, 0xb6, 0xae, 0x80, 0x8d, 0x2b, 0xc2, - 0xef, 0x0c, 0x02, 0x33, 0x9c, 0x7e, 0x77, 0x98, 0xad, 0x56, 0x2a, 0xbd, 0x74, 0xd4, 0x37, 0x90, - 0xf4, 0x88, 0x0e, 0x1c, 0xed, 0xbd, 0xe8, 0x80, 0xb0, 0x3c, 0x75, 0x24, 0xf7, 0xe4, 0x29, 0xb1, - 0xf6, 0x5c, 0x67, 0x55, 0x7e, 0x50, 0x5b, 0x7a, 0x47, 0xb2, 0xc5, 0xfc, 0x1e, 0xf4, 0xdf, 0x2e, - 0x51, 0xbb, 0x9b, 0x47, 0xd2, 0x21, 0x32, 0xc1, 0x11, 0x6d, 0xe5, 0xae, 0xb4, 0x80, 0x3f, 0x82, - 0x40, 0xd2, 0x15, 0xe6, 0xd4, 0x1b, 0x04, 0x99, 0xb4, 0xb4, 0xd5, 0x70, 0xcf, 0x3d, 0xa3, 0x2e, - 0xe7, 0x79, 0x8e, 0xda, 0x79, 0xd7, 0x02, 0xd3, 0x3b, 0x5b, 0xa3, 0x36, 0x2b, 0x7b, 0xd2, 0x82, - 0xf0, 0x1d, 0x8c, 0xf6, 0x13, 0xd4, 0xa5, 0xac, 0x12, 0xfc, 0xc5, 0x62, 0x1c, 0xfc, 0x17, 0xb3, - 0x57, 0x27, 0x8d, 0xe3, 0x29, 0xde, 0xf8, 0xd4, 0xeb, 0xf8, 0x94, 0x0e, 0x7a, 0xa9, 0x72, 0x35, - 0x8d, 0x8c, 0xb0, 0x9e, 0x74, 0x28, 0x7c, 0x02, 0x3e, 0x7d, 0x0f, 0x9d, 0xce, 0xfe, 0xef, 0xbe, - 0xa5, 0x8b, 0xbe, 0xf9, 0x97, 0xde, 0xfb, 0x19, 0x00, 0x00, 0xff, 0xff, 0x93, 0x68, 0x0f, 0xcf, - 0xb7, 0x05, 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, } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index d67d3ba55..54bfd4f64 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -38,6 +38,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 { diff --git a/bolt/servers.go b/bolt/servers.go index 63bd7c801..3521c294f 100644 --- a/bolt/servers.go +++ b/bolt/servers.go @@ -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 +} diff --git a/bolt/servers_test.go b/bolt/servers_test.go index 4105a2413..bca2e1e74 100644 --- a/bolt/servers_test.go +++ b/bolt/servers_test.go @@ -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) diff --git a/chronograf.go b/chronograf.go index 337bffc49..d9c24c5ae 100644 --- a/chronograf.go +++ b/chronograf.go @@ -270,6 +270,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` diff --git a/server/kapacitors.go b/server/kapacitors.go index 33b172b8f..e34aec50c 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -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) diff --git a/server/swagger.json b/server/swagger.json index 13047913b..f66ebb659 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -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": { diff --git a/ui/.eslintrc b/ui/.eslintrc index b2b1adb83..996fed43a 100644 --- a/ui/.eslintrc +++ b/ui/.eslintrc @@ -40,7 +40,7 @@ }, }, rules: { - 'quotes': [0, "double"], + 'quotes': [1, 'single'], 'func-style': 0, 'func-names': 0, 'arrow-parens': 0, diff --git a/ui/src/index.js b/ui/src/index.js index dd4844022..e6f7665c0 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -109,7 +109,8 @@ const Root = React.createClass({ - + + diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js index 8877dc9d3..7348df664 100644 --- a/ui/src/kapacitor/actions/view/index.js +++ b/ui/src/kapacitor/actions/view/index.js @@ -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', diff --git a/ui/src/kapacitor/components/AlertOutputs.js b/ui/src/kapacitor/components/AlertOutputs.js deleted file mode 100644 index 921b29177..000000000 --- a/ui/src/kapacitor/components/AlertOutputs.js +++ /dev/null @@ -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 ( -
-
-

Configure Alert Endpoints

-
-
-
- - -
-
-
-
-
-
-
- {this.renderAlertConfig(selectedEndpoint)} -
-
-
-
- ) - }, - - 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 - } - case 'smtp': { - return - } - case 'slack': { - return - } - case 'victorops': { - return - } - case 'telegram': { - return - } - case 'opsgenie': { - return - } - case 'pagerduty': { - return - } - case 'hipchat': { - return - } - case 'sensu': { - return - } - case 'talk': { - return - } - } - }, -}) - -export default AlertOutputs diff --git a/ui/src/kapacitor/components/AlertTabs.js b/ui/src/kapacitor/components/AlertTabs.js new file mode 100644 index 000000000..3d02ceed8 --- /dev/null +++ b/ui/src/kapacitor/components/AlertTabs.js @@ -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: ( this.handleSaveConfig('alerta', p)} config={this.getSection(configSections, 'alerta')} />), + }, + { + type: 'SMTP', + component: ( this.handleSaveConfig('smtp', p)} config={this.getSection(configSections, 'smtp')} />), + }, + { + type: 'Slack', + component: ( this.handleSaveConfig('slack', p)} onTest={test} config={this.getSection(configSections, 'slack')} />), + }, + { + type: 'VictorOps', + component: ( this.handleSaveConfig('victorops', p)} config={this.getSection(configSections, 'victorops')} />), + }, + { + type: 'Telegram', + component: ( this.handleSaveConfig('telegram', p)} config={this.getSection(configSections, 'telegram')} />), + }, + { + type: 'OpsGenie', + component: ( this.handleSaveConfig('opsgenie', p)} config={this.getSection(configSections, 'opsgenie')} />), + }, + { + type: 'PagerDuty', + component: ( this.handleSaveConfig('pagerduty', p)} config={this.getSection(configSections, 'pagerduty')} />), + }, + { + type: 'HipChat', + component: ( this.handleSaveConfig('hipchat', p)} config={this.getSection(configSections, 'hipchat')} />), + }, + { + type: 'Sensu', + component: ( this.handleSaveConfig('sensu', p)} config={this.getSection(configSections, 'sensu')} />), + }, + { + type: 'Talk', + component: ( this.handleSaveConfig('talk', p)} config={this.getSection(configSections, 'talk')} />), + }, + ] + + return ( +
+
+
+

Configure Alert Endpoints

+
+
+ + + + { + tabs.map((t, i) => ({tabs[i].type})) + } + + + { + tabs.map((t, i) => ({t.component})) + } + + +
+ ) + } +} + +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 diff --git a/ui/src/kapacitor/components/AlertaConfig.js b/ui/src/kapacitor/components/AlertaConfig.js deleted file mode 100644 index dd37d4483..000000000 --- a/ui/src/kapacitor/components/AlertaConfig.js +++ /dev/null @@ -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 ( -
-

Alerta Alert

-
-
-

- Have alerts sent to Alerta -

- -
- - this.environment = r} defaultValue={environment || ''}> -
- -
- - this.origin = r} defaultValue={origin || ''}> -
- -
- - this.token = r} defaultValue={token || ''}> - Note: a value of true indicates the Alerta Token has been set -
- -
- - this.url = r} defaultValue={url || ''}> -
- -
- -
-
-
- ) - }, -}) - -export default AlertaConfig diff --git a/ui/src/kapacitor/components/HipChatConfig.js b/ui/src/kapacitor/components/HipChatConfig.js deleted file mode 100644 index 42c392e93..000000000 --- a/ui/src/kapacitor/components/HipChatConfig.js +++ /dev/null @@ -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 ( -
-

HipChat Alert

-
-

Send alert messages to HipChat.

-
-
- - this.url = r} - defaultValue={subdomain && subdomain.length ? subdomain : ''} - /> -
- -
- - this.room = r} - defaultValue={room || ''} - /> -
- -
- - this.token = r} - defaultValue={token || ''} - /> - -
- -
- -
-
-
- ) - }, -}) - -export default HipchatConfig diff --git a/ui/src/kapacitor/components/KapacitorForm.js b/ui/src/kapacitor/components/KapacitorForm.js index afa6bf331..5db261ad1 100644 --- a/ui/src/kapacitor/components/KapacitorForm.js +++ b/ui/src/kapacitor/components/KapacitorForm.js @@ -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({
-
+
+
+

Connection Details

+
-

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

-
-

Connect Kapacitor to Source

-

{source.url}

-
-
+
+ onChange={onInputChange} + spellCheck="false">
-
+
+ onChange={onInputChange} + spellCheck="false">
-
+
+ onChange={onInputChange} + spellCheck="false">
-
+
- - + +
-
-
-
+
{this.renderAlertOutputs()}
@@ -121,26 +95,50 @@ const KapacitorForm = React.createClass({
) - }, + } // TODO: move these to another page. they dont belong on this page renderAlertOutputs() { const {exists, kapacitor, addFlashMessage, source} = this.props if (exists) { - return + return } return (
+
+

Configure Alert Endpoints

+
-

Configure Alert Endpoints


Set your Kapacitor connection info to configure alerting endpoints.

) - }, -}) + } +} + +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 diff --git a/ui/src/kapacitor/components/PagerDutyConfig.js b/ui/src/kapacitor/components/PagerDutyConfig.js deleted file mode 100644 index d6e96b958..000000000 --- a/ui/src/kapacitor/components/PagerDutyConfig.js +++ /dev/null @@ -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 ( -
-

PagerDuty Alert

-
-

You can have alerts sent to PagerDuty by entering info below.

-
-
- - this.serviceKey = r} defaultValue={serviceKey || ''}> - -
- -
- - this.url = r} defaultValue={url || ''}> -
- -
- -
-
-
- ) - }, -}) - -export default PagerDutyConfig diff --git a/ui/src/kapacitor/components/SMTPConfig.js b/ui/src/kapacitor/components/SMTPConfig.js deleted file mode 100644 index 2a61c5165..000000000 --- a/ui/src/kapacitor/components/SMTPConfig.js +++ /dev/null @@ -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 ( -
-

SMTP Alert

-
-

You can have alerts sent to an email address by setting up an SMTP endpoint.

-
-
- - this.host = r} defaultValue={host || ''}> -
- -
- - this.port = r} defaultValue={port || ''}> -
- -
- - this.from = r} defaultValue={from || ''}> -
- -
- - this.username = r} defaultValue={username || ''}> -
- -
- - this.password = r} defaultValue={`${password}`}> -
- -
- -
-
-
- ) - }, -}) - -export default SMTPConfig diff --git a/ui/src/kapacitor/components/SensuConfig.js b/ui/src/kapacitor/components/SensuConfig.js deleted file mode 100644 index b3c9bad5e..000000000 --- a/ui/src/kapacitor/components/SensuConfig.js +++ /dev/null @@ -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 ( -
-

Sensu Alert

-
-

Have alerts sent to Sensu.

-
-
- - this.source = r} defaultValue={source || ''}> -
- -
- - this.addr = r} defaultValue={addr || ''}> -
- -
- -
-
-
- ) - }, -}) - -export default SensuConfig diff --git a/ui/src/kapacitor/components/SlackConfig.js b/ui/src/kapacitor/components/SlackConfig.js deleted file mode 100644 index c6ab16eec..000000000 --- a/ui/src/kapacitor/components/SlackConfig.js +++ /dev/null @@ -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 ( -
-

Slack Alert

-
-

Post alerts to a Slack channel.

-
-
- - this.url = r} defaultValue={url || ''}> - -
- -
- - this.channel = r} defaultValue={channel || ''}> -
- -
- Send Test Message - -
-
-
- ) - }, -}) - -export default SlackConfig diff --git a/ui/src/kapacitor/components/TalkConfig.js b/ui/src/kapacitor/components/TalkConfig.js deleted file mode 100644 index 05ff984c3..000000000 --- a/ui/src/kapacitor/components/TalkConfig.js +++ /dev/null @@ -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 ( -
-

Talk Alert

-
-

Have alerts sent to Talk.

-
-
- - this.url = r} defaultValue={url || ''}> - -
- -
- - this.author = r} defaultValue={author || ''}> -
- -
- -
-
-
- ) - }, -}) - -export default TalkConfig diff --git a/ui/src/kapacitor/components/TelegramConfig.js b/ui/src/kapacitor/components/TelegramConfig.js deleted file mode 100644 index b29f950aa..000000000 --- a/ui/src/kapacitor/components/TelegramConfig.js +++ /dev/null @@ -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 ( -
-

Telegram Alert

-
-

- Send alert messages to a Telegram bot. -

-
-
- - this.token = r} - defaultValue={token || ''}> - - -
- -
- - this.chatID = r} - defaultValue={chatID || ''}> - -
- -
- -
-
- this.parseModeMarkdown = r} /> - -
-
- this.parseModeHTML = r} /> - -
-
-
- -
-
- this.disableWebPagePreview = r} /> - -
-
- -
-
- this.disableNotification = r} /> - -
-
- -
- -
-
-
- ) - }, -}) - -export default TelegramConfig diff --git a/ui/src/kapacitor/components/VictorOpsConfig.js b/ui/src/kapacitor/components/VictorOpsConfig.js deleted file mode 100644 index b821117d7..000000000 --- a/ui/src/kapacitor/components/VictorOpsConfig.js +++ /dev/null @@ -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 ( -
-

VictorOps Alert

-
-

Have alerts sent to VictorOps.

-
-
- - this.apiKey = r} defaultValue={apiKey || ''}> - -
- -
- - this.routingKey = r} defaultValue={routingKey || ''}> -
- -
- - this.url = r} defaultValue={url || ''}> -
- -
- -
-
-
- ) - }, -}) - -export default VictorOpsConfig diff --git a/ui/src/kapacitor/components/config/AlertaConfig.js b/ui/src/kapacitor/components/config/AlertaConfig.js new file mode 100644 index 000000000..37df70114 --- /dev/null +++ b/ui/src/kapacitor/components/config/AlertaConfig.js @@ -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 ( +
+
+ + this.environment = r} defaultValue={environment || ''}> +
+ +
+ + this.origin = r} defaultValue={origin || ''}> +
+ +
+ + this.token = r} defaultValue={token || ''}> + +
+ +
+ + this.url = r} defaultValue={url || ''}> +
+ +
+ +
+
+ ) + }, +}) + +export default AlertaConfig diff --git a/ui/src/kapacitor/components/config/HipChatConfig.js b/ui/src/kapacitor/components/config/HipChatConfig.js new file mode 100644 index 000000000..421e71627 --- /dev/null +++ b/ui/src/kapacitor/components/config/HipChatConfig.js @@ -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 ( +
+
+ + this.url = r} + defaultValue={subdomain && subdomain.length ? subdomain : ''} + /> +
+ +
+ + this.room = r} + defaultValue={room || ''} + /> +
+ +
+ + this.token = r} + defaultValue={token || ''} + /> + +
+ +
+ +
+
+ ) + }, +}) + +export default HipchatConfig diff --git a/ui/src/kapacitor/components/OpsGenieConfig.js b/ui/src/kapacitor/components/config/OpsGenieConfig.js similarity index 75% rename from ui/src/kapacitor/components/OpsGenieConfig.js rename to ui/src/kapacitor/components/config/OpsGenieConfig.js index 53833d707..9d626a207 100644 --- a/ui/src/kapacitor/components/OpsGenieConfig.js +++ b/ui/src/kapacitor/components/config/OpsGenieConfig.js @@ -64,25 +64,20 @@ const OpsGenieConfig = React.createClass({ const {currentTeams, currentRecipients} = this.state return ( -
-

OpsGenie Alert

-
-

Have alerts sent to OpsGenie.

-
-
- - this.apiKey = r} defaultValue={apiKey || ''}> - -
+ +
+ + this.apiKey = r} defaultValue={apiKey || ''}> + +
- - + + -
- -
- -
+
+ +
+ ) }, }) diff --git a/ui/src/kapacitor/components/config/PagerDutyConfig.js b/ui/src/kapacitor/components/config/PagerDutyConfig.js new file mode 100644 index 000000000..224289524 --- /dev/null +++ b/ui/src/kapacitor/components/config/PagerDutyConfig.js @@ -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 ( +
+
+ + this.serviceKey = r} defaultValue={serviceKey || ''}> + +
+ +
+ + this.url = r} defaultValue={url || ''}> +
+ +
+ +
+
+ ) + }, +}) + +export default PagerDutyConfig diff --git a/ui/src/kapacitor/components/config/SMTPConfig.js b/ui/src/kapacitor/components/config/SMTPConfig.js new file mode 100644 index 000000000..545875c27 --- /dev/null +++ b/ui/src/kapacitor/components/config/SMTPConfig.js @@ -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 ( +
+
+ + this.host = r} defaultValue={host || ''}> +
+ +
+ + this.port = r} defaultValue={port || ''}> +
+ +
+ + this.from = r} defaultValue={from || ''}> +
+ +
+ + this.username = r} defaultValue={username || ''}> +
+ +
+ + this.password = r} defaultValue={`${password}`}> +
+ +
+ +
+
+ ) + }, +}) + +export default SMTPConfig diff --git a/ui/src/kapacitor/components/config/SensuConfig.js b/ui/src/kapacitor/components/config/SensuConfig.js new file mode 100644 index 000000000..588c789c3 --- /dev/null +++ b/ui/src/kapacitor/components/config/SensuConfig.js @@ -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 ( +
+
+ + this.source = r} defaultValue={source || ''}> +
+ +
+ + this.addr = r} defaultValue={addr || ''}> +
+ +
+ +
+
+ ) + }, +}) + +export default SensuConfig diff --git a/ui/src/kapacitor/components/config/SlackConfig.js b/ui/src/kapacitor/components/config/SlackConfig.js new file mode 100644 index 000000000..b9855b939 --- /dev/null +++ b/ui/src/kapacitor/components/config/SlackConfig.js @@ -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 ( +
+
+ + this.url = r} defaultValue={url || ''}> + +
+ +
+ + this.channel = r} defaultValue={channel || ''}> +
+ +
+ Send Test Message + +
+
+ ) + }, +}) + +export default SlackConfig diff --git a/ui/src/kapacitor/components/config/TalkConfig.js b/ui/src/kapacitor/components/config/TalkConfig.js new file mode 100644 index 000000000..240346d1c --- /dev/null +++ b/ui/src/kapacitor/components/config/TalkConfig.js @@ -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 ( +
+
+ + this.url = r} defaultValue={url || ''}> + +
+ +
+ + this.author = r} defaultValue={author || ''}> +
+ +
+ +
+
+ ) + }, +}) + +export default TalkConfig diff --git a/ui/src/kapacitor/components/config/TelegramConfig.js b/ui/src/kapacitor/components/config/TelegramConfig.js new file mode 100644 index 000000000..52ff92b8b --- /dev/null +++ b/ui/src/kapacitor/components/config/TelegramConfig.js @@ -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 ( +
+

+ You need a Telegram Bot to use this endpoint +

+
+ + this.token = r} + defaultValue={token || ''}> + + +
+ +
+ + this.chatID = r} + defaultValue={chatID || ''}> + +
+ +
+ +
+
+ this.parseModeMarkdown = r} /> + +
+
+ this.parseModeHTML = r} /> + +
+
+
+ +
+
+ this.disableWebPagePreview = r} /> + +
+
+ +
+
+ this.disableNotification = r} /> + +
+
+ +
+ +
+
+ ) + }, +}) + +export default TelegramConfig diff --git a/ui/src/kapacitor/components/config/VictorOpsConfig.js b/ui/src/kapacitor/components/config/VictorOpsConfig.js new file mode 100644 index 000000000..73e11eac4 --- /dev/null +++ b/ui/src/kapacitor/components/config/VictorOpsConfig.js @@ -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 ( +
+
+ + this.apiKey = r} defaultValue={apiKey || ''}> + +
+ +
+ + this.routingKey = r} defaultValue={routingKey || ''}> +
+ +
+ + this.url = r} defaultValue={url || ''}> +
+ +
+ +
+
+ ) + }, +}) + +export default VictorOpsConfig diff --git a/ui/src/kapacitor/components/config/index.js b/ui/src/kapacitor/components/config/index.js new file mode 100644 index 000000000..8cf004a07 --- /dev/null +++ b/ui/src/kapacitor/components/config/index.js @@ -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, +} diff --git a/ui/src/kapacitor/containers/KapacitorPage.js b/ui/src/kapacitor/containers/KapacitorPage.js index d7a1651ef..012eec4f3 100644 --- a/ui/src/kapacitor/containers/KapacitorPage.js +++ b/ui/src/kapacitor/containers/KapacitorPage.js @@ -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 ( - - ) - }, + } 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 ( + + ) + } +} + +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 diff --git a/ui/src/kapacitor/containers/KapacitorRulePage.js b/ui/src/kapacitor/containers/KapacitorRulePage.js index df55b05d9..141f486e5 100644 --- a/ui/src/kapacitor/containers/KapacitorRulePage.js +++ b/ui/src/kapacitor/containers/KapacitorRulePage.js @@ -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) => { @@ -117,4 +116,4 @@ function mapDispatchToProps(dispatch) { } } -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage)) +export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage) diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index a7344a239..0ac448dcc 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -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) } diff --git a/ui/src/shared/actions/sources.js b/ui/src/shared/actions/sources.js index 9fb356ffb..0711ebb02 100644 --- a/ui/src/shared/actions/sources.js +++ b/ui/src/shared/actions/sources.js @@ -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) => { @@ -42,3 +61,19 @@ export const removeAndLoadSources = (source) => async (dispatch) => { 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) +} diff --git a/ui/src/shared/apis/index.js b/ui/src/shared/apis/index.js index d42f63885..f66da174a 100644 --- a/ui/src/shared/apis/index.js +++ b/ui/src/shared/apis/index.js @@ -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, }, }) } diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 8d18c6a55..a18589cd8 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -1,7 +1,99 @@ -import React, {PropTypes} from 'react' +import React, {Component, PropTypes} from 'react' +import {Link} from 'react-router' import classnames from 'classnames' + import OnClickOutside from 'shared/components/OnClickOutside' +class Dropdown extends Component { + constructor(props) { + super(props) + this.state = { + isOpen: false, + } + + this.handleClickOutside = ::this.handleClickOutside + this.handleSelection = ::this.handleSelection + this.toggleMenu = ::this.toggleMenu + this.handleAction = ::this.handleAction + } + + static defaultProps = { + actions: [], + buttonSize: 'btn-sm', + buttonColor: 'btn-info', + menuWidth: '100%', + } + + handleClickOutside() { + this.setState({isOpen: false}) + } + + handleSelection(item) { + this.toggleMenu() + this.props.onChoose(item) + } + + toggleMenu(e) { + if (e) { + e.stopPropagation() + } + this.setState({isOpen: !this.state.isOpen}) + } + + handleAction(e, action, item) { + e.stopPropagation() + action.handler(item) + } + + render() { + const {items, selected, className, iconName, actions, addNew, buttonSize, buttonColor, menuWidth} = this.props + const {isOpen} = this.state + + return ( +
+
+ {iconName ? : null} + {selected} + +
+ {isOpen ? +
    + {items.map((item, i) => { + return ( +
  • + this.handleSelection(item)}> + {item.text} + + {actions.length > 0 ? +
    + {actions.map((action) => { + return ( + + ) + })} +
    + : null} +
  • + ) + })} + { + addNew ? +
  • + + {addNew.text} + +
  • : + null + } +
+ : null} +
+ ) + } +} + const { arrayOf, shape, @@ -9,79 +101,26 @@ const { func, } = PropTypes -const Dropdown = React.createClass({ - propTypes: { - items: arrayOf(shape({ - text: string.isRequired, - })).isRequired, - onChoose: func.isRequired, - selected: string.isRequired, - iconName: string, - className: string, - }, - getInitialState() { - return { - isOpen: false, - } - }, - getDefaultProps() { - return { - actions: [], - } - }, - handleClickOutside() { - this.setState({isOpen: false}) - }, - handleSelection(item) { - this.toggleMenu() - this.props.onChoose(item) - }, - toggleMenu(e) { - if (e) { - e.stopPropagation() - } - this.setState({isOpen: !this.state.isOpen}) - }, - handleAction(e, action, item) { - e.stopPropagation() - action.handler(item) - }, - render() { - const self = this - const {items, selected, className, iconName, actions} = self.props - - return ( -
-
- {iconName ? : null} - {selected} - -
- {self.state.isOpen ? - - : null} -
- ) - }, -}) +Dropdown.propTypes = { + actions: arrayOf(shape({ + icon: string.isRequired, + text: string.isRequired, + handler: func.isRequired, + })), + items: arrayOf(shape({ + text: string.isRequired, + })).isRequired, + onChoose: func.isRequired, + addNew: shape({ + url: string.isRequired, + text: string.isRequired, + }), + selected: string.isRequired, + iconName: string, + className: string, + buttonSize: string, + buttonColor: string, + menuWidth: string, +} export default OnClickOutside(Dropdown) diff --git a/ui/src/shared/reducers/sources.js b/ui/src/shared/reducers/sources.js index cfef11c40..2c4781c80 100644 --- a/ui/src/shared/reducers/sources.js +++ b/ui/src/shared/reducers/sources.js @@ -1,3 +1,5 @@ +import _ from 'lodash' + const getInitialState = () => [] const initialState = getInitialState() @@ -25,6 +27,27 @@ const sourcesReducer = (state = initialState, action) => { }) : state return [...updatedSources, source] } + + case 'LOAD_KAPACITORS': { + const {source, kapacitors} = action.payload + const sourceIndex = state.findIndex((s) => s.id === source.id) + const updatedSources = _.cloneDeep(state) + if (updatedSources[sourceIndex]) { + updatedSources[sourceIndex].kapacitors = kapacitors + } + return updatedSources + } + + case 'SET_ACTIVE_KAPACITOR': { + const {kapacitor} = action.payload + const updatedSources = _.cloneDeep(state) + updatedSources.forEach((source) => { + source.kapacitors.forEach((k, i) => { + source.kapacitors[i].active = (k.id === kapacitor.id) + }) + }) + return updatedSources + } } return state diff --git a/ui/src/side_nav/containers/SideNav.js b/ui/src/side_nav/containers/SideNav.js index c98893d2d..6a30c20f2 100644 --- a/ui/src/side_nav/containers/SideNav.js +++ b/ui/src/side_nav/containers/SideNav.js @@ -56,8 +56,6 @@ const SideNav = React.createClass({ - InfluxDB - Kapacitor { showLogout ? ( diff --git a/ui/src/sources/components/InfluxTable.js b/ui/src/sources/components/InfluxTable.js new file mode 100644 index 000000000..7af217be4 --- /dev/null +++ b/ui/src/sources/components/InfluxTable.js @@ -0,0 +1,132 @@ +import React, {PropTypes} from 'react' +import {Link, withRouter} from 'react-router' + +import Dropdown from 'shared/components/Dropdown' + +const kapacitorDropdown = (kapacitors, source, router, setActiveKapacitor) => { + if (!kapacitors || kapacitors.length === 0) { + return ( + Add Kapacitor + ) + } + const kapacitorItems = kapacitors.map((k) => { + return { + text: k.name, + resource: `/sources/${source.id}/kapacitors/${k.id}`, + kapacitor: k, + } + }) + + const activeKapacitor = kapacitors.find((k) => k.active) + + let selected = '' + if (activeKapacitor) { + selected = activeKapacitor.name + } else { + selected = kapacitorItems[0].text + } + + return ( + setActiveKapacitor(item.kapacitor)} + addNew={{ + url: `/sources/${source.id}/kapacitors/new`, + text: "Add Kapacitor", + }} + actions={ + [{ + icon: "pencil", + text: "edit", + handler: (item) => { + router.push(`${item.resource}/edit`) + }, + }]} + selected={selected} + /> + ) +} + +const InfluxTable = ({ + sources, + source, + handleDeleteSource, + location, + router, + setActiveKapacitor, +}) => ( +
+
+
+
+

InfluxDB Sources

+ Add Source +
+
+ + + + + + + + + + + { + sources.map((s) => { + return ( + + + + + + + ) + }) + } + +
NameHostKapacitor
{s.name} {s.default ? Default : null}{s.url} + { + kapacitorDropdown(s.kapacitors, s, router, setActiveKapacitor) + } + + Connect + +
+
+
+
+
+) + +const { + array, + func, + shape, + string, +} = PropTypes + +InfluxTable.propTypes = { + handleDeleteSource: func.isRequired, + location: shape({ + pathname: string.isRequired, + }).isRequired, + router: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + source: shape({ + id: string.isRequired, + links: shape({ + proxy: string.isRequired, + self: string.isRequired, + }), + }), + sources: array.isRequired, + setActiveKapacitor: func.isRequired, +} + +export default withRouter(InfluxTable) diff --git a/ui/src/sources/containers/ManageSources.js b/ui/src/sources/containers/ManageSources.js index 715afb06f..7136b0dfd 100644 --- a/ui/src/sources/containers/ManageSources.js +++ b/ui/src/sources/containers/ManageSources.js @@ -1,52 +1,35 @@ -import React, {PropTypes} from 'react' -import {withRouter, Link} from 'react-router' -import {getKapacitor} from 'shared/apis' -import {removeAndLoadSources} from 'src/shared/actions/sources' +import React, {Component, PropTypes} from 'react' import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' -const { - array, - func, - shape, - string, -} = PropTypes +import { + removeAndLoadSources, + fetchKapacitorsAsync, + setActiveKapacitorAsync, +} from 'src/shared/actions/sources' -export const ManageSources = React.createClass({ - propTypes: { - location: shape({ - pathname: string.isRequired, - }).isRequired, - source: shape({ - id: string.isRequired, - links: shape({ - proxy: string.isRequired, - self: string.isRequired, - }), - }), - sources: array, - addFlashMessage: func, - removeAndLoadSources: func, - }, +import InfluxTable from '../components/InfluxTable' - getInitialState() { - return { - kapacitors: {}, - } - }, +class ManageSources extends Component { + constructor(props) { + super(props) + + this.handleDeleteSource = ::this.handleDeleteSource + } componentDidMount() { - const updates = [] - const kapas = {} this.props.sources.forEach((source) => { - const prom = getKapacitor(source).then((kapacitor) => { - kapas[source.id] = kapacitor + this.props.fetchKapacitors(source) + }) + } + + componentDidUpdate(prevProps) { + if (prevProps.sources.length !== this.props.sources.length) { + this.props.sources.forEach((source) => { + this.props.fetchKapacitors(source) }) - updates.push(prom) - }) - Promise.all(updates).then(() => { - this.setState({kapacitors: kapas}) - }) - }, + } + } handleDeleteSource(source) { const {addFlashMessage} = this.props @@ -56,81 +39,65 @@ export const ManageSources = React.createClass({ } catch (e) { addFlashMessage({type: 'error', text: 'Could not remove source from Chronograf'}) } - }, + } render() { - const {kapacitors} = this.state - const {sources} = this.props - const {pathname} = this.props.location - const numSources = sources.length - const sourcesTitle = `${numSources} ${numSources === 1 ? 'Source' : 'Sources'}` + const {sources, source, setActiveKapacitor} = this.props return (
-

InfluxDB Sources

+

Configuration

-
-
- -
-
-

{sourcesTitle}

- Add New Source -
-
-
- - - - - - - - - - - { - sources.map((source) => { - const kapacitorName = kapacitors[source.id] ? kapacitors[source.id].name : '' - return ( - - - - - - - ) - }) - } - -
NameHostKapacitor
{source.name}{source.default ? Default : null}{source.url}{kapacitorName ? kapacitorName : "--"} - - Connect - -
-
-
-
-
-
+
) - }, -}) - -function mapStateToProps(state) { - return { - sources: state.sources, } } -export default connect(mapStateToProps, {removeAndLoadSources})(withRouter(ManageSources)) +const { + array, + func, + shape, + string, +} = PropTypes + +ManageSources.propTypes = { + source: shape({ + id: string.isRequired, + links: shape({ + proxy: string.isRequired, + self: string.isRequired, + }), + }), + sources: array, + addFlashMessage: func, + removeAndLoadSources: func.isRequired, + fetchKapacitors: func.isRequired, + setActiveKapacitor: func.isRequired, +} + +const mapStateToProps = ({sources}) => ({ + sources, +}) + +const mapDispatchToProps = (dispatch) => ({ + removeAndLoadSources: bindActionCreators(removeAndLoadSources, dispatch), + fetchKapacitors: bindActionCreators(fetchKapacitorsAsync, dispatch), + setActiveKapacitor: bindActionCreators(setActiveKapacitorAsync, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(ManageSources) diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 3b7c63c27..3dba7aced 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -45,6 +45,8 @@ @import 'components/query-maker'; // Pages + +@import 'pages/config-endpoints'; @import 'pages/signup'; @import 'pages/auth-page'; @import 'pages/kapacitor'; diff --git a/ui/src/style/pages/config-endpoints.scss b/ui/src/style/pages/config-endpoints.scss new file mode 100644 index 000000000..37aac2ca1 --- /dev/null +++ b/ui/src/style/pages/config-endpoints.scss @@ -0,0 +1,65 @@ +/* + Kapacitor Config Styles + ---------------------------------------------- +*/ + +$config-endpoint-tab-height: 40px; + +$config-endpoint-tab-text: $g10-wolf; +$config-endpoint-tab-text-hover: $g15-platinum; +$config-endpoint-tab-text-active: $g18-cloud; + +$config-endpoint-tab-bg: transparent; +$config-endpoint-tab-bg-hover: $g3-castle; +$config-endpoint-tab-bg-active: $g3-castle; + +.config-endpoint { + display: flex; + align-items: stretch; +} +.config-endpoint--tabs { + flex: 0 0 0; + display: flex; + + .btn-group.tab-group { + overflow: hidden; + background-color: $g2-kevlar; + border-radius: $radius 0 0 $radius; + margin: 0; + display: flex; + flex: 1 0 0; + flex-direction: column; + align-items: stretch; + + .btn.tab { + color: $config-endpoint-tab-text; + background-color: $config-endpoint-tab-bg; + border-radius: 0; + height: $config-endpoint-tab-height; + border: 0; + padding: 0 40px 0 15px; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 500; + font-size: 16px; + + &:first-child {border-top-left-radius: $radius;} + + &:hover { + color: $config-endpoint-tab-text-hover; + background-color: $config-endpoint-tab-bg-hover; + } + &.active { + color: $config-endpoint-tab-text-active; + background-color: $config-endpoint-tab-bg-active; + } + } + } +} +.config-endpoint--tab-contents { + flex: 1 0 0; + background-color: $config-endpoint-tab-bg-active; + border-radius: 0 $radius $radius 0; + padding: 16px 42px; +} \ No newline at end of file diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss index 0c61f8d6b..3aae03605 100644 --- a/ui/src/style/unsorted.scss +++ b/ui/src/style/unsorted.scss @@ -154,3 +154,28 @@ div:nth-child(3) {left: 100%; animation: refreshingSpinnerC 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;} } + +/* + Kapacitor selector dropdown + ---------------------------------------------- +*/ +.dropdown .dropdown-toggle.btn-xs { + height: 22px !important; + line-height: 22px !important; + padding: 0 9px !important; +} +.dropdown-selected { + display: inline-block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 15px; +} +.dropdown.sources--kapacitor-selector { + .dropdown-toggle { + width: 160px; + } +} + +