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
-
-
-
- Alert Enpoint
-
- Alerta
- HipChat
- OpsGenie
- PagerDuty
- Sensu
- Slack
- SMTP
- Talk
- Telegram
- VictorOps
-
-
-
-
-
-
-
-
- {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 (
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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}
-
)
- },
+ }
// 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.
-
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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 .
-
-
-
- )
- },
-})
-
-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.
-
-
- )
- },
-})
-
-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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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.
-
-
+
+ Save
+
+
)
},
})
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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 (
+
+ )
+ },
+})
+
+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 ?
+
+ : 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
+
+
+
+
+
+ Name
+ Host
+ Kapacitor
+
+
+
+
+ {
+ sources.map((s) => {
+ return (
+
+ {s.name} {s.default ? Default : null}
+ {s.url}
+
+ {
+ kapacitorDropdown(s.kapacitors, s, router, setActiveKapacitor)
+ }
+
+
+ Connect
+ handleDeleteSource(s)}>
+
+
+ )
+ })
+ }
+
+
+
+
+
+
+)
+
+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
-
-
-
-
-
-
- Name
- Host
- Kapacitor
-
-
-
-
- {
- sources.map((source) => {
- const kapacitorName = kapacitors[source.id] ? kapacitors[source.id].name : ''
- return (
-
- {source.name}{source.default ? Default : null}
- {source.url}
- {kapacitorName ? kapacitorName : "--"}
-
-
- Connect
- this.handleDeleteSource(source)}>
-
-
- )
- })
- }
-
-
-
-
-
-
-
+
)
- },
-})
-
-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;
+ }
+}
+
+