Merge pull request #1302 from influxdata/multiple-kapacitors
show multiple kapacitorspull/1324/head
commit
5ae6d238b1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -24,14 +24,9 @@ type ServersStore struct {
|
|||
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
|
||||
var srcs []chronograf.Server
|
||||
if err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
|
||||
var src chronograf.Server
|
||||
if err := internal.UnmarshalServer(v, &src); err != nil {
|
||||
return err
|
||||
}
|
||||
srcs = append(srcs, src)
|
||||
return nil
|
||||
}); err != nil {
|
||||
var err error
|
||||
srcs, err = s.all(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -53,6 +48,10 @@ func (s *ServersStore) Add(ctx context.Context, src chronograf.Server) (chronogr
|
|||
}
|
||||
src.ID = int(seq)
|
||||
|
||||
// make the newly added source "active"
|
||||
s.resetActiveServer(ctx, tx)
|
||||
src.Active = true
|
||||
|
||||
if v, err := internal.MarshalServer(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(src.ID), v); err != nil {
|
||||
|
@ -106,6 +105,11 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
|
|||
return chronograf.ErrServerNotFound
|
||||
}
|
||||
|
||||
// only one server can be active at a time
|
||||
if src.Active {
|
||||
s.resetActiveServer(ctx, tx)
|
||||
}
|
||||
|
||||
if v, err := internal.MarshalServer(src); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(src.ID), v); err != nil {
|
||||
|
@ -118,3 +122,39 @@ func (s *ServersStore) Update(ctx context.Context, src chronograf.Server) error
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServersStore) all(ctx context.Context, tx *bolt.Tx) ([]chronograf.Server, error) {
|
||||
var srcs []chronograf.Server
|
||||
if err := tx.Bucket(ServersBucket).ForEach(func(k, v []byte) error {
|
||||
var src chronograf.Server
|
||||
if err := internal.UnmarshalServer(v, &src); err != nil {
|
||||
return err
|
||||
}
|
||||
srcs = append(srcs, src)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return srcs, err
|
||||
}
|
||||
return srcs, nil
|
||||
}
|
||||
|
||||
// resetActiveServer unsets the Active flag on all sources
|
||||
func (s *ServersStore) resetActiveServer(ctx context.Context, tx *bolt.Tx) error {
|
||||
b := tx.Bucket(ServersBucket)
|
||||
srcs, err := s.all(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, other := range srcs {
|
||||
if other.Active {
|
||||
other.Active = false
|
||||
if v, err := internal.MarshalServer(other); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(itob(other.ID), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ func TestServerStore(t *testing.T) {
|
|||
Username: "marty",
|
||||
Password: "I❤️ jennifer parker",
|
||||
URL: "toyota-hilux.lyon-estates.local",
|
||||
Active: false,
|
||||
},
|
||||
chronograf.Server{
|
||||
Name: "HipToBeSquare",
|
||||
|
@ -34,6 +35,7 @@ func TestServerStore(t *testing.T) {
|
|||
Username: "calvinklein",
|
||||
Password: "chuck b3rry",
|
||||
URL: "toyota-hilux.lyon-estates.local",
|
||||
Active: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -72,6 +74,21 @@ func TestServerStore(t *testing.T) {
|
|||
t.Fatalf("server 1 update error: got %v, expected %v", src.Name, "Enchantment Under the Sea Dance")
|
||||
}
|
||||
|
||||
// Attempt to make two active sources
|
||||
srcs[0].Active = true
|
||||
srcs[1].Active = true
|
||||
if err := s.Update(ctx, srcs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := s.Update(ctx, srcs[1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if actual, err := s.Get(ctx, srcs[0].ID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if actual.Active == true {
|
||||
t.Fatal("Able to set two active servers when only one should be permitted")
|
||||
}
|
||||
|
||||
// Delete an server.
|
||||
if err := s.Delete(ctx, srcs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -17,6 +17,7 @@ type postKapacitorRequest struct {
|
|||
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
|
||||
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
|
||||
Password string `json:"password,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (p *postKapacitorRequest) Valid() error {
|
||||
|
@ -47,6 +48,7 @@ type kapacitor struct {
|
|||
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
|
||||
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
|
||||
Password string `json:"password,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
|
||||
}
|
||||
|
||||
|
@ -81,6 +83,7 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
|
|||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
URL: *req.URL,
|
||||
Active: req.Active,
|
||||
}
|
||||
|
||||
if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
|
||||
|
@ -102,6 +105,7 @@ func newKapacitor(srv chronograf.Server) kapacitor {
|
|||
Username: srv.Username,
|
||||
Password: srv.Password,
|
||||
URL: srv.URL,
|
||||
Active: srv.Active,
|
||||
Links: kapaLinks{
|
||||
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
|
||||
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
|
||||
|
@ -217,6 +221,7 @@ type patchKapacitorRequest struct {
|
|||
URL *string `json:"url,omitempty"` // URL for the kapacitor
|
||||
Username *string `json:"username,omitempty"` // Username for kapacitor auth
|
||||
Password *string `json:"password,omitempty"`
|
||||
Active *bool `json:"active"`
|
||||
}
|
||||
|
||||
func (p *patchKapacitorRequest) Valid() error {
|
||||
|
@ -276,6 +281,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
|
|||
if req.Username != nil {
|
||||
srv.Username = *req.Username
|
||||
}
|
||||
if req.Active != nil {
|
||||
srv.Active = *req.Active
|
||||
}
|
||||
|
||||
if err := h.ServersStore.Update(ctx, srv); err != nil {
|
||||
msg := fmt.Sprintf("Error updating kapacitor ID %d", id)
|
||||
|
|
|
@ -1154,7 +1154,7 @@
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"summary": "Configured kapacitors",
|
||||
"summary": "Retrieve list of configured kapacitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of kapacitors",
|
||||
|
@ -1239,7 +1239,7 @@
|
|||
}
|
||||
],
|
||||
"summary": "Configured kapacitors",
|
||||
"description": "These kapacitors are used for monitoring and alerting.",
|
||||
"description": "Retrieve information on a single kapacitor instance",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Kapacitor connection information",
|
||||
|
@ -1334,7 +1334,8 @@
|
|||
"required": true
|
||||
}
|
||||
],
|
||||
"summary": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
|
||||
"summary": "Remove Kapacitor backend",
|
||||
"description": "This specific kapacitor will be removed. All associated rule resources will also be removed from the store.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "kapacitor has been removed."
|
||||
|
@ -1683,7 +1684,7 @@
|
|||
"kapacitors",
|
||||
"proxy"
|
||||
],
|
||||
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
|
||||
"description": "DELETE to `path` of kapacitor. The response and status code from kapacitor is directly returned.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -2388,6 +2389,7 @@
|
|||
"id": "4",
|
||||
"name": "kapa",
|
||||
"url": "http://localhost:9092",
|
||||
"active": false,
|
||||
"links": {
|
||||
"proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy",
|
||||
"self": "/chronograf/v1/sources/4/kapacitors/4",
|
||||
|
@ -2417,6 +2419,10 @@
|
|||
"format": "url",
|
||||
"description": "URL for the kapacitor backend (e.g. http://localhost:9092)"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the kapacitor is the current kapacitor being used for a source"
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
},
|
||||
rules: {
|
||||
'quotes': [0, "double"],
|
||||
'quotes': [1, 'single'],
|
||||
'func-style': 0,
|
||||
'func-names': 0,
|
||||
'arrow-parens': 0,
|
||||
|
|
|
@ -109,7 +109,8 @@ const Root = React.createClass({
|
|||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route path="kubernetes" component={KubernetesPage} />
|
||||
<Route path="kapacitor-config" component={KapacitorPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import uuid from 'node-uuid'
|
||||
import {getKapacitor} from 'src/shared/apis'
|
||||
import {getActiveKapacitor} from 'src/shared/apis'
|
||||
import {publishNotification} from 'src/shared/actions/notifications'
|
||||
import {
|
||||
getRules,
|
||||
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
export function fetchRule(source, ruleID) {
|
||||
return (dispatch) => {
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
getActiveKapacitor(source).then((kapacitor) => {
|
||||
getRule(kapacitor, ruleID).then(({data: rule}) => {
|
||||
dispatch({
|
||||
type: 'LOAD_RULE',
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
|
||||
import AlertaConfig from './AlertaConfig'
|
||||
import HipChatConfig from './HipChatConfig'
|
||||
import OpsGenieConfig from './OpsGenieConfig'
|
||||
import PagerDutyConfig from './PagerDutyConfig'
|
||||
import SensuConfig from './SensuConfig'
|
||||
import SlackConfig from './SlackConfig'
|
||||
import SMTPConfig from './SMTPConfig'
|
||||
import TalkConfig from './TalkConfig'
|
||||
import TelegramConfig from './TelegramConfig'
|
||||
import VictorOpsConfig from './VictorOpsConfig'
|
||||
|
||||
const AlertOutputs = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
kapacitor: PropTypes.shape({
|
||||
url: PropTypes.string.isRequired,
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
addFlashMessage: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedEndpoint: 'smtp',
|
||||
configSections: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
|
||||
this.refreshKapacitorConfig(nextProps.kapacitor)
|
||||
}
|
||||
},
|
||||
|
||||
refreshKapacitorConfig(kapacitor) {
|
||||
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
|
||||
this.setState({configSections: sections})
|
||||
}).catch(() => {
|
||||
this.setState({configSections: null})
|
||||
this.props.addFlashMessage({type: 'error', text: `There was an error getting the Kapacitor config`})
|
||||
})
|
||||
},
|
||||
|
||||
getSection(sections, section) {
|
||||
return _.get(sections, [section, 'elements', '0'], null)
|
||||
},
|
||||
|
||||
handleSaveConfig(section, properties) {
|
||||
if (section !== '') {
|
||||
const propsToSend = this.sanitizeProperties(section, properties)
|
||||
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
|
||||
}).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: `There was an error saving the kapacitor config`})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
changeSelectedEndpoint(e) {
|
||||
this.setState({
|
||||
selectedEndpoint: e.target.value,
|
||||
})
|
||||
},
|
||||
|
||||
handleTest(section, properties) {
|
||||
const propsToSend = this.sanitizeProperties(section, properties)
|
||||
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
|
||||
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
|
||||
}).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: `There was an error testing the slack alert`})
|
||||
})
|
||||
},
|
||||
|
||||
sanitizeProperties(section, properties) {
|
||||
const cleanProps = Object.assign({}, properties, {enabled: true})
|
||||
const {redacted} = this.getSection(this.state.configSections, section)
|
||||
if (redacted && redacted.length) {
|
||||
redacted.forEach((badProp) => {
|
||||
if (properties[badProp] === 'true') {
|
||||
delete cleanProps[badProp]
|
||||
}
|
||||
})
|
||||
}
|
||||
return cleanProps
|
||||
},
|
||||
|
||||
render() {
|
||||
const {configSections, selectedEndpoint} = this.state
|
||||
if (!configSections) { // could use this state to conditionally render spinner or error message
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-body">
|
||||
<h4 className="text-center no-user-select">Configure Alert Endpoints</h4>
|
||||
<br/>
|
||||
<div className="row">
|
||||
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2">
|
||||
<label htmlFor="alert-endpoint" className="sr-only">Alert Enpoint</label>
|
||||
<select value={this.state.selectedEndpoint} className="form-control" id="source" onChange={this.changeSelectedEndpoint}>
|
||||
<option value="alerta">Alerta</option>
|
||||
<option value="hipchat">HipChat</option>
|
||||
<option value="opsgenie">OpsGenie</option>
|
||||
<option value="pagerduty">PagerDuty</option>
|
||||
<option value="sensu">Sensu</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="talk">Talk</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="victorops">VictorOps</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
|
||||
<hr/>
|
||||
</div>
|
||||
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
|
||||
{this.renderAlertConfig(selectedEndpoint)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderAlertConfig(endpoint) {
|
||||
const {configSections} = this.state
|
||||
const save = (properties) => {
|
||||
this.handleSaveConfig(endpoint, properties)
|
||||
}
|
||||
const test = (properties) => {
|
||||
this.handleTest(endpoint, properties)
|
||||
}
|
||||
|
||||
switch (endpoint) {
|
||||
case 'alerta': {
|
||||
return <AlertaConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'smtp': {
|
||||
return <SMTPConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'slack': {
|
||||
return <SlackConfig onSave={save} onTest={test} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'victorops': {
|
||||
return <VictorOpsConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'telegram': {
|
||||
return <TelegramConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'opsgenie': {
|
||||
return <OpsGenieConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'pagerduty': {
|
||||
return <PagerDutyConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'hipchat': {
|
||||
return <HipChatConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'sensu': {
|
||||
return <SensuConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
case 'talk': {
|
||||
return <TalkConfig onSave={save} config={this.getSection(configSections, endpoint)} />
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default AlertOutputs
|
|
@ -0,0 +1,189 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
|
||||
|
||||
import {
|
||||
AlertaConfig,
|
||||
HipChatConfig,
|
||||
OpsGenieConfig,
|
||||
PagerDutyConfig,
|
||||
SensuConfig,
|
||||
SlackConfig,
|
||||
SMTPConfig,
|
||||
TalkConfig,
|
||||
TelegramConfig,
|
||||
VictorOpsConfig,
|
||||
} from './config'
|
||||
|
||||
class AlertTabs extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectedEndpoint: 'smtp',
|
||||
configSections: null,
|
||||
}
|
||||
this.refreshKapacitorConfig = ::this.refreshKapacitorConfig
|
||||
this.getSection = ::this.getSection
|
||||
this.handleSaveConfig = ::this.handleSaveConfig
|
||||
this.handleTest = ::this.handleTest
|
||||
this.sanitizeProperties = ::this.sanitizeProperties
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
|
||||
this.refreshKapacitorConfig(nextProps.kapacitor)
|
||||
}
|
||||
}
|
||||
|
||||
refreshKapacitorConfig(kapacitor) {
|
||||
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
|
||||
this.setState({configSections: sections})
|
||||
}).catch(() => {
|
||||
this.setState({configSections: null})
|
||||
this.props.addFlashMessage({type: 'error', text: 'There was an error getting the Kapacitor config'})
|
||||
})
|
||||
}
|
||||
|
||||
getSection(sections, section) {
|
||||
return _.get(sections, [section, 'elements', '0'], null)
|
||||
}
|
||||
|
||||
handleSaveConfig(section, properties) {
|
||||
if (section !== '') {
|
||||
const propsToSend = this.sanitizeProperties(section, properties)
|
||||
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
|
||||
}).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: 'There was an error saving the kapacitor config'})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTest(section, properties) {
|
||||
const propsToSend = this.sanitizeProperties(section, properties)
|
||||
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
|
||||
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
|
||||
}).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: 'There was an error testing the slack alert'})
|
||||
})
|
||||
}
|
||||
|
||||
sanitizeProperties(section, properties) {
|
||||
const cleanProps = Object.assign({}, properties, {enabled: true})
|
||||
const {redacted} = this.getSection(this.state.configSections, section)
|
||||
if (redacted && redacted.length) {
|
||||
redacted.forEach((badProp) => {
|
||||
if (properties[badProp] === 'true') {
|
||||
delete cleanProps[badProp]
|
||||
}
|
||||
})
|
||||
}
|
||||
return cleanProps
|
||||
}
|
||||
|
||||
render() {
|
||||
const {configSections} = this.state
|
||||
|
||||
if (!configSections) {
|
||||
return null
|
||||
}
|
||||
|
||||
const test = (properties) => {
|
||||
this.handleTest('slack', properties)
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
type: 'Alerta',
|
||||
component: (<AlertaConfig onSave={(p) => this.handleSaveConfig('alerta', p)} config={this.getSection(configSections, 'alerta')} />),
|
||||
},
|
||||
{
|
||||
type: 'SMTP',
|
||||
component: (<SMTPConfig onSave={(p) => this.handleSaveConfig('smtp', p)} config={this.getSection(configSections, 'smtp')} />),
|
||||
},
|
||||
{
|
||||
type: 'Slack',
|
||||
component: (<SlackConfig onSave={(p) => this.handleSaveConfig('slack', p)} onTest={test} config={this.getSection(configSections, 'slack')} />),
|
||||
},
|
||||
{
|
||||
type: 'VictorOps',
|
||||
component: (<VictorOpsConfig onSave={(p) => this.handleSaveConfig('victorops', p)} config={this.getSection(configSections, 'victorops')} />),
|
||||
},
|
||||
{
|
||||
type: 'Telegram',
|
||||
component: (<TelegramConfig onSave={(p) => this.handleSaveConfig('telegram', p)} config={this.getSection(configSections, 'telegram')} />),
|
||||
},
|
||||
{
|
||||
type: 'OpsGenie',
|
||||
component: (<OpsGenieConfig onSave={(p) => this.handleSaveConfig('opsgenie', p)} config={this.getSection(configSections, 'opsgenie')} />),
|
||||
},
|
||||
{
|
||||
type: 'PagerDuty',
|
||||
component: (<PagerDutyConfig onSave={(p) => this.handleSaveConfig('pagerduty', p)} config={this.getSection(configSections, 'pagerduty')} />),
|
||||
},
|
||||
{
|
||||
type: 'HipChat',
|
||||
component: (<HipChatConfig onSave={(p) => this.handleSaveConfig('hipchat', p)} config={this.getSection(configSections, 'hipchat')} />),
|
||||
},
|
||||
{
|
||||
type: 'Sensu',
|
||||
component: (<SensuConfig onSave={(p) => this.handleSaveConfig('sensu', p)} config={this.getSection(configSections, 'sensu')} />),
|
||||
},
|
||||
{
|
||||
type: 'Talk',
|
||||
component: (<TalkConfig onSave={(p) => this.handleSaveConfig('talk', p)} config={this.getSection(configSections, 'talk')} />),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Configure Alert Endpoints</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs tabContentsClass="config-endpoint">
|
||||
<TabList customClass="config-endpoint--tabs">
|
||||
{
|
||||
tabs.map((t, i) => (<Tab key={tabs[i].type}>{tabs[i].type}</Tab>))
|
||||
}
|
||||
</TabList>
|
||||
<TabPanels customClass="config-endpoint--tab-contents">
|
||||
{
|
||||
tabs.map((t, i) => (<TabPanel key={tabs[i].type}>{t.component}</TabPanel>))
|
||||
}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
AlertTabs.propTypes = {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
}).isRequired,
|
||||
kapacitor: shape({
|
||||
url: string.isRequired,
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
addFlashMessage: func.isRequired,
|
||||
}
|
||||
|
||||
export default AlertTabs
|
|
@ -1,71 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const AlertaConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
environment: PropTypes.string,
|
||||
origin: PropTypes.string,
|
||||
token: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
environment: this.environment.value,
|
||||
origin: this.origin.value,
|
||||
token: this.token.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {environment, origin, token, url} = this.props.config.options
|
||||
|
||||
return (
|
||||
<div className="col-xs-12">
|
||||
<h4 className="text-center no-user-select">Alerta Alert</h4>
|
||||
<br/>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<p className="no-user-select">
|
||||
Have alerts sent to Alerta
|
||||
</p>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="environment">Environment</label>
|
||||
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="origin">Origin</label>
|
||||
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">Token</label>
|
||||
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
|
||||
<span>Note: a value of <code>true</code> indicates the Alerta Token has been set</span>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">User</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default AlertaConfig
|
|
@ -1,100 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
|
||||
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const HipchatConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
room: string.isRequired,
|
||||
token: bool.isRequired,
|
||||
url: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
room: this.room.value,
|
||||
url: `https://${this.url.value}.hipchat.com/v2/room`,
|
||||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {url, room, token} = options
|
||||
|
||||
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">HipChat Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Send alert messages to HipChat.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">Subdomain</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
type="text"
|
||||
placeholder="your-subdomain"
|
||||
ref={(r) => this.url = r}
|
||||
defaultValue={subdomain && subdomain.length ? subdomain : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="room">Room</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="room"
|
||||
type="text"
|
||||
placeholder="your-hipchat-room"
|
||||
ref={(r) => this.room = r}
|
||||
defaultValue={room || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">
|
||||
Token
|
||||
<QuestionMarkTooltip
|
||||
tipID="token"
|
||||
tipContent={HIPCHAT_TOKEN_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="token"
|
||||
type="text"
|
||||
placeholder="your-hipchat-token"
|
||||
ref={(r) => this.token = r}
|
||||
defaultValue={token || ''}
|
||||
/>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default HipchatConfig
|
|
@ -1,31 +1,9 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import AlertOutputs from './AlertOutputs'
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
const KapacitorForm = React.createClass({
|
||||
propTypes: {
|
||||
onSubmit: func.isRequired,
|
||||
onInputChange: func.isRequired,
|
||||
onReset: func.isRequired,
|
||||
kapacitor: shape({
|
||||
url: string.isRequired,
|
||||
name: string.isRequired,
|
||||
username: string,
|
||||
password: string,
|
||||
}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
exists: bool.isRequired,
|
||||
},
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import AlertTabs from './AlertTabs'
|
||||
|
||||
class KapacitorForm extends Component {
|
||||
render() {
|
||||
const {onInputChange, onReset, kapacitor, source, onSubmit} = this.props
|
||||
const {onInputChange, onReset, kapacitor, onSubmit} = this.props
|
||||
const {url, name, username, password} = kapacitor
|
||||
|
||||
return (
|
||||
|
@ -42,21 +20,15 @@ const KapacitorForm = React.createClass({
|
|||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="col-md-3">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Connection Details</h2>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<p className="no-user-select">
|
||||
Kapacitor is used as the monitoring and alerting agent.
|
||||
This page will let you configure which Kapacitor to use and
|
||||
set up alert end points like email, Slack, and others.
|
||||
</p>
|
||||
<hr/>
|
||||
<h4 className="text-center no-user-select">Connect Kapacitor to Source</h4>
|
||||
<h4 className="text-center">{source.url}</h4>
|
||||
<br/>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div>
|
||||
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="url">Kapacitor URL</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -64,10 +36,11 @@ const KapacitorForm = React.createClass({
|
|||
name="url"
|
||||
placeholder={url}
|
||||
value={url}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2 col-md-4 col-md-offset-0">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -75,10 +48,11 @@ const KapacitorForm = React.createClass({
|
|||
name="name"
|
||||
placeholder={name}
|
||||
value={name}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-4 col-sm-offset-2 col-md-4 col-md-offset-2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -86,10 +60,11 @@ const KapacitorForm = React.createClass({
|
|||
name="username"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
onChange={onInputChange}>
|
||||
onChange={onInputChange}
|
||||
spellCheck="false">
|
||||
</input>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-4 col-md-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -99,21 +74,20 @@ const KapacitorForm = React.createClass({
|
|||
placeholder="password"
|
||||
value={password}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-info" type="button" onClick={onReset}>Reset to Default</button>
|
||||
<button className="btn btn-success" type="submit">Connect Kapacitor</button>
|
||||
<button className="btn btn-info" type="button" onClick={onReset}>Reset</button>
|
||||
<button className="btn btn-success" type="submit">Connect</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="col-md-9">
|
||||
{this.renderAlertOutputs()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -121,26 +95,50 @@ const KapacitorForm = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: move these to another page. they dont belong on this page
|
||||
renderAlertOutputs() {
|
||||
const {exists, kapacitor, addFlashMessage, source} = this.props
|
||||
|
||||
if (exists) {
|
||||
return <AlertOutputs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
|
||||
return <AlertTabs source={source} kapacitor={kapacitor} addFlashMessage={addFlashMessage} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">Configure Alert Endpoints</h2>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<h4 className="text-center">Configure Alert Endpoints</h4>
|
||||
<br/>
|
||||
<p className="text-center">Set your Kapacitor connection info to configure alerting endpoints.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
KapacitorForm.propTypes = {
|
||||
onSubmit: func.isRequired,
|
||||
onInputChange: func.isRequired,
|
||||
onReset: func.isRequired,
|
||||
kapacitor: shape({
|
||||
url: string.isRequired,
|
||||
name: string.isRequired,
|
||||
username: string,
|
||||
password: string,
|
||||
}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
exists: bool.isRequired,
|
||||
}
|
||||
|
||||
export default KapacitorForm
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const PagerDutyConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
'service-key': PropTypes.bool.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
serviceKey: this.serviceKey.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {url} = options
|
||||
const serviceKey = options['service-key']
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">PagerDuty Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">You can have alerts sent to PagerDuty by entering info below.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="service-key">Service Key</label>
|
||||
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">PagerDuty URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default PagerDutyConfig
|
|
@ -1,74 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SMTPConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
host: PropTypes.string,
|
||||
port: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.bool,
|
||||
from: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
host: this.host.value,
|
||||
port: this.port.value,
|
||||
from: this.from.value,
|
||||
username: this.username.value,
|
||||
password: this.password.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {host, port, from, username, password} = this.props.config.options
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">SMTP Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">You can have alerts sent to an email address by setting up an SMTP endpoint.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-host">SMTP Host</label>
|
||||
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-port">SMTP Port</label>
|
||||
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="smtp-from">From Email</label>
|
||||
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-user">User</label>
|
||||
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-password">Password</label>
|
||||
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SMTPConfig
|
|
@ -1,53 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SensuConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
source: PropTypes.string.isRequired,
|
||||
addr: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
source: this.source.value,
|
||||
addr: this.addr.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {source, addr} = this.props.config.options
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">Sensu Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Have alerts sent to Sensu.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="source">Source</label>
|
||||
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="address">Address</label>
|
||||
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SensuConfig
|
|
@ -1,76 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SlackConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
url: PropTypes.bool.isRequired,
|
||||
channel: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onTest: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
testEnabled: !!this.props.config.options.url,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
testEnabled: !!nextProps.config.options.url,
|
||||
})
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
url: this.url.value,
|
||||
channel: this.channel.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
handleTest(e) {
|
||||
e.preventDefault()
|
||||
this.props.onTest({
|
||||
url: this.url.value,
|
||||
channel: this.channel.value,
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {url, channel} = this.props.config.options
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">Slack Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Post alerts to a Slack channel.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
|
||||
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="slack-channel">Slack Channel (optional)</label>
|
||||
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
|
||||
<button className="btn btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SlackConfig
|
|
@ -1,61 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const {
|
||||
bool,
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
const TalkConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
url: bool.isRequired,
|
||||
author_name: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
url: this.url.value,
|
||||
author_name: this.author.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {url, author_name: author} = this.props.config.options
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">Talk Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Have alerts sent to Talk.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="author">Author Name</label>
|
||||
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TalkConfig
|
|
@ -1,142 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
|
||||
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const TelegramConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
'chat-id': string.isRequired,
|
||||
'disable-notification': bool.isRequired,
|
||||
'disable-web-page-preview': bool.isRequired,
|
||||
'parse-mode': string.isRequired,
|
||||
token: bool.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
let parseMode
|
||||
if (this.parseModeHTML.checked) {
|
||||
parseMode = 'HTML'
|
||||
}
|
||||
if (this.parseModeMarkdown.checked) {
|
||||
parseMode = 'Markdown'
|
||||
}
|
||||
|
||||
const properties = {
|
||||
'chat-id': this.chatID.value,
|
||||
'disable-notification': this.disableNotification.checked,
|
||||
'disable-web-page-preview': this.disableWebPagePreview.checked,
|
||||
'parse-mode': parseMode,
|
||||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {token} = options
|
||||
const chatID = options['chat-id']
|
||||
const disableNotification = options['disable-notification']
|
||||
const disableWebPagePreview = options['disable-web-page-preview']
|
||||
const parseMode = options['parse-mode']
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">Telegram Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">
|
||||
Send alert messages to a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram bot</a>.
|
||||
</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">
|
||||
Token
|
||||
<QuestionMarkTooltip
|
||||
tipID="token"
|
||||
tipContent={TELEGRAM_TOKEN_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="token"
|
||||
type="text"
|
||||
placeholder="your-telegram-token"
|
||||
ref={(r) => this.token = r}
|
||||
defaultValue={token || ''}>
|
||||
</input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="chat-id">
|
||||
Chat ID
|
||||
<QuestionMarkTooltip
|
||||
tipID="chat-id"
|
||||
tipContent={TELEGRAM_CHAT_ID_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="chat-id"
|
||||
type="text"
|
||||
placeholder="your-telegram-chat-id"
|
||||
ref={(r) => this.chatID = r}
|
||||
defaultValue={chatID || ''}>
|
||||
</input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="parseMode">Select the alert message format</label>
|
||||
<div className="form-control-static">
|
||||
<div className="radio">
|
||||
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
|
||||
<label htmlFor="parseModeMarkdown">Markdown</label>
|
||||
</div>
|
||||
<div className="radio">
|
||||
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
|
||||
<label htmlFor="parseModeHTML">HTML</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
|
||||
<label htmlFor="disableWebPagePreview">
|
||||
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
|
||||
<label htmlFor="disableNotification">
|
||||
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TelegramConfig
|
|
@ -1,64 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const VictorOpsConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
'api-key': PropTypes.bool,
|
||||
'routing-key': PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
'api-key': this.apiKey.value,
|
||||
'routing-key': this.routingKey.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const apiKey = options['api-key']
|
||||
const routingKey = options['routing-key']
|
||||
const {url} = options
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">VictorOps Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Have alerts sent to VictorOps.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="routing-key">Routing Key</label>
|
||||
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">VictorOps URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default VictorOpsConfig
|
|
@ -0,0 +1,63 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const AlertaConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
environment: PropTypes.string,
|
||||
origin: PropTypes.string,
|
||||
token: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
environment: this.environment.value,
|
||||
origin: this.origin.value,
|
||||
token: this.token.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {environment, origin, token, url} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="environment">Environment</label>
|
||||
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="origin">Origin</label>
|
||||
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">Token</label>
|
||||
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the Alerta Token has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">User</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default AlertaConfig
|
|
@ -0,0 +1,95 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
|
||||
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const HipchatConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
room: string.isRequired,
|
||||
token: bool.isRequired,
|
||||
url: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
room: this.room.value,
|
||||
url: `https://${this.url.value}.hipchat.com/v2/room`,
|
||||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {url, room, token} = options
|
||||
|
||||
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">Subdomain</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
type="text"
|
||||
placeholder="your-subdomain"
|
||||
ref={(r) => this.url = r}
|
||||
defaultValue={subdomain && subdomain.length ? subdomain : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="room">Room</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="room"
|
||||
type="text"
|
||||
placeholder="your-hipchat-room"
|
||||
ref={(r) => this.room = r}
|
||||
defaultValue={room || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">
|
||||
Token
|
||||
<QuestionMarkTooltip
|
||||
tipID="token"
|
||||
tipContent={HIPCHAT_TOKEN_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="token"
|
||||
type="text"
|
||||
placeholder="your-hipchat-token"
|
||||
ref={(r) => this.token = r}
|
||||
defaultValue={token || ''}
|
||||
/>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default HipchatConfig
|
|
@ -64,25 +64,20 @@ const OpsGenieConfig = React.createClass({
|
|||
const {currentTeams, currentRecipients} = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-center no-user-select">OpsGenie Alert</h4>
|
||||
<br/>
|
||||
<p className="no-user-select">Have alerts sent to OpsGenie.</p>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
|
||||
</div>
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the OpsGenie API key has been set</label>
|
||||
</div>
|
||||
|
||||
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
|
||||
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
|
||||
<TagInput title="Teams" onAddTag={this.handleAddTeam} onDeleteTag={this.handleDeleteTeam} tags={currentTeams} />
|
||||
<TagInput title="Recipients" onAddTag={this.handleAddRecipient} onDeleteTag={this.handleDeleteRecipient} tags={currentRecipients} />
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const PagerDutyConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
'service-key': PropTypes.bool.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
serviceKey: this.serviceKey.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {url} = options
|
||||
const serviceKey = options['service-key']
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="service-key">Service Key</label>
|
||||
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">PagerDuty URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default PagerDutyConfig
|
|
@ -0,0 +1,69 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SMTPConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
host: PropTypes.string,
|
||||
port: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.bool,
|
||||
from: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
host: this.host.value,
|
||||
port: this.port.value,
|
||||
from: this.from.value,
|
||||
username: this.username.value,
|
||||
password: this.password.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {host, port, from, username, password} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-host">SMTP Host</label>
|
||||
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-port">SMTP Port</label>
|
||||
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="smtp-from">From Email</label>
|
||||
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-user">User</label>
|
||||
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-password">Password</label>
|
||||
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SMTPConfig
|
|
@ -0,0 +1,48 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SensuConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
source: PropTypes.string.isRequired,
|
||||
addr: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
source: this.source.value,
|
||||
addr: this.addr.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {source, addr} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="source">Source</label>
|
||||
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="address">Address</label>
|
||||
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SensuConfig
|
|
@ -0,0 +1,71 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const SlackConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
url: PropTypes.bool.isRequired,
|
||||
channel: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onTest: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
testEnabled: !!this.props.config.options.url,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
testEnabled: !!nextProps.config.options.url,
|
||||
})
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
url: this.url.value,
|
||||
channel: this.channel.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
handleTest(e) {
|
||||
e.preventDefault()
|
||||
this.props.onTest({
|
||||
url: this.url.value,
|
||||
channel: this.channel.value,
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {url, channel} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
|
||||
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="slack-channel">Slack Channel (optional)</label>
|
||||
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
|
||||
<button className="btn btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default SlackConfig
|
|
@ -0,0 +1,56 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const {
|
||||
bool,
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
const TalkConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
url: bool.isRequired,
|
||||
author_name: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
url: this.url.value,
|
||||
author_name: this.author.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {url, author_name: author} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="author">Author Name</label>
|
||||
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TalkConfig
|
|
@ -0,0 +1,138 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
|
||||
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const TelegramConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: shape({
|
||||
options: shape({
|
||||
'chat-id': string.isRequired,
|
||||
'disable-notification': bool.isRequired,
|
||||
'disable-web-page-preview': bool.isRequired,
|
||||
'parse-mode': string.isRequired,
|
||||
token: bool.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
let parseMode
|
||||
if (this.parseModeHTML.checked) {
|
||||
parseMode = 'HTML'
|
||||
}
|
||||
if (this.parseModeMarkdown.checked) {
|
||||
parseMode = 'Markdown'
|
||||
}
|
||||
|
||||
const properties = {
|
||||
'chat-id': this.chatID.value,
|
||||
'disable-notification': this.disableNotification.checked,
|
||||
'disable-web-page-preview': this.disableWebPagePreview.checked,
|
||||
'parse-mode': parseMode,
|
||||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const {token} = options
|
||||
const chatID = options['chat-id']
|
||||
const disableNotification = options['disable-notification']
|
||||
const disableWebPagePreview = options['disable-web-page-preview']
|
||||
const parseMode = options['parse-mode']
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<p className="no-user-select">
|
||||
You need a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram Bot</a> to use this endpoint
|
||||
</p>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="token">
|
||||
Token
|
||||
<QuestionMarkTooltip
|
||||
tipID="token"
|
||||
tipContent={TELEGRAM_TOKEN_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="token"
|
||||
type="text"
|
||||
placeholder="your-telegram-token"
|
||||
ref={(r) => this.token = r}
|
||||
defaultValue={token || ''}>
|
||||
</input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="chat-id">
|
||||
Chat ID
|
||||
<QuestionMarkTooltip
|
||||
tipID="chat-id"
|
||||
tipContent={TELEGRAM_CHAT_ID_TIP}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="chat-id"
|
||||
type="text"
|
||||
placeholder="your-telegram-chat-id"
|
||||
ref={(r) => this.chatID = r}
|
||||
defaultValue={chatID || ''}>
|
||||
</input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="parseMode">Select the alert message format</label>
|
||||
<div className="form-control-static">
|
||||
<div className="radio">
|
||||
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
|
||||
<label htmlFor="parseModeMarkdown">Markdown</label>
|
||||
</div>
|
||||
<div className="radio">
|
||||
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
|
||||
<label htmlFor="parseModeHTML">HTML</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
|
||||
<label htmlFor="disableWebPagePreview">
|
||||
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-control-static">
|
||||
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
|
||||
<label htmlFor="disableNotification">
|
||||
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default TelegramConfig
|
|
@ -0,0 +1,59 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const VictorOpsConfig = React.createClass({
|
||||
propTypes: {
|
||||
config: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
'api-key': PropTypes.bool,
|
||||
'routing-key': PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
handleSaveAlert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
'api-key': this.apiKey.value,
|
||||
'routing-key': this.routingKey.value,
|
||||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {options} = this.props.config
|
||||
const apiKey = options['api-key']
|
||||
const routingKey = options['routing-key']
|
||||
const {url} = options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
|
||||
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="routing-key">Routing Key</label>
|
||||
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">VictorOps URL</label>
|
||||
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button className="btn btn-block btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default VictorOpsConfig
|
|
@ -0,0 +1,23 @@
|
|||
import AlertaConfig from './AlertaConfig'
|
||||
import HipChatConfig from './HipChatConfig'
|
||||
import OpsGenieConfig from './OpsGenieConfig'
|
||||
import PagerDutyConfig from './PagerDutyConfig'
|
||||
import SensuConfig from './SensuConfig'
|
||||
import SlackConfig from './SlackConfig'
|
||||
import SMTPConfig from './SMTPConfig'
|
||||
import TalkConfig from './TalkConfig'
|
||||
import TelegramConfig from './TelegramConfig'
|
||||
import VictorOpsConfig from './VictorOpsConfig'
|
||||
|
||||
export {
|
||||
AlertaConfig,
|
||||
HipChatConfig,
|
||||
OpsGenieConfig,
|
||||
PagerDutyConfig,
|
||||
SensuConfig,
|
||||
SlackConfig,
|
||||
SMTPConfig,
|
||||
TalkConfig,
|
||||
TelegramConfig,
|
||||
VictorOpsConfig,
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {
|
||||
getKapacitor,
|
||||
createKapacitor,
|
||||
|
@ -7,26 +7,13 @@ import {
|
|||
} from 'shared/apis'
|
||||
import KapacitorForm from '../components/KapacitorForm'
|
||||
|
||||
const defaultName = "My Kapacitor"
|
||||
const kapacitorPort = "9092"
|
||||
const defaultName = 'My Kapacitor'
|
||||
const kapacitorPort = '9092'
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
export const KapacitorPage = React.createClass({
|
||||
propTypes: {
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
url: string.isRequired,
|
||||
}),
|
||||
addFlashMessage: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
class KapacitorPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
kapacitor: {
|
||||
url: this._parseKapacitorURL(),
|
||||
name: defaultName,
|
||||
|
@ -35,39 +22,27 @@ export const KapacitorPage = React.createClass({
|
|||
},
|
||||
exists: false,
|
||||
}
|
||||
},
|
||||
|
||||
this.handleInputChange = ::this.handleInputChange
|
||||
this.handleSubmit = ::this.handleSubmit
|
||||
this.handleResetToDefaults = ::this.handleResetToDefaults
|
||||
this._parseKapacitorURL = ::this._parseKapacitorURL
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {source} = this.props
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
if (!kapacitor) {
|
||||
return
|
||||
}
|
||||
const {source, params: {id}} = this.props
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
getKapacitor(source, id).then((kapacitor) => {
|
||||
this.setState({kapacitor, exists: true}, () => {
|
||||
pingKapacitor(kapacitor).catch(() => {
|
||||
this.props.addFlashMessage({type: 'error', text: 'Could not connect to Kapacitor. Check settings.'})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
const {kapacitor, exists} = this.state
|
||||
|
||||
return (
|
||||
<KapacitorForm
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
onReset={this.handleResetToDefaults}
|
||||
kapacitor={kapacitor}
|
||||
source={source}
|
||||
addFlashMessage={addFlashMessage}
|
||||
exists={exists}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const {value, name} = e.target
|
||||
|
@ -76,8 +51,7 @@ export const KapacitorPage = React.createClass({
|
|||
const update = {[name]: value.trim()}
|
||||
return {kapacitor: {...prevState.kapacitor, ...update}}
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
|
@ -99,7 +73,7 @@ export const KapacitorPage = React.createClass({
|
|||
addFlashMessage({type: 'error', text: 'There was a problem creating the Kapacitor record'})
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleResetToDefaults(e) {
|
||||
e.preventDefault()
|
||||
|
@ -111,14 +85,48 @@ export const KapacitorPage = React.createClass({
|
|||
}
|
||||
|
||||
this.setState({kapacitor: {...defaultState}})
|
||||
},
|
||||
}
|
||||
|
||||
_parseKapacitorURL() {
|
||||
const parser = document.createElement('a')
|
||||
parser.href = this.props.source.url
|
||||
|
||||
return `${parser.protocol}//${parser.hostname}:${kapacitorPort}`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
const {kapacitor, exists} = this.state
|
||||
|
||||
return (
|
||||
<KapacitorForm
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
onReset={this.handleResetToDefaults}
|
||||
kapacitor={kapacitor}
|
||||
source={source}
|
||||
addFlashMessage={addFlashMessage}
|
||||
exists={exists}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
KapacitorPage.propTypes = {
|
||||
addFlashMessage: func,
|
||||
params: shape({
|
||||
id: string,
|
||||
}).isRequired,
|
||||
source: shape({
|
||||
id: string.isRequired,
|
||||
url: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default KapacitorPage
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import * as kapacitorActionCreators from '../actions/view'
|
||||
import * as queryActionCreators from '../../data_explorer/actions/view'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {getKapacitor, getKapacitorConfig} from 'shared/apis/index'
|
||||
import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index'
|
||||
import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'
|
||||
import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
|
||||
|
||||
|
@ -53,7 +52,7 @@ export const KapacitorRulePage = React.createClass({
|
|||
kapacitorActions.loadDefaultRule()
|
||||
}
|
||||
|
||||
getKapacitor(source).then((kapacitor) => {
|
||||
getActiveKapacitor(source).then((kapacitor) => {
|
||||
this.setState({kapacitor})
|
||||
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
|
||||
const enabledAlerts = Object.keys(sections).filter((section) => {
|
||||
|
@ -117,4 +116,4 @@ function mapDispatchToProps(dispatch) {
|
|||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {getKapacitor} from 'src/shared/apis'
|
||||
import {getActiveKapacitor} from 'src/shared/apis'
|
||||
import * as kapacitorActionCreators from '../actions/view'
|
||||
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
|
||||
|
||||
|
@ -18,7 +18,7 @@ class KapacitorRulesPage extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
getKapacitor(this.props.source).then((kapacitor) => {
|
||||
getActiveKapacitor(this.props.source).then((kapacitor) => {
|
||||
if (kapacitor) {
|
||||
this.props.actions.fetchRules(kapacitor)
|
||||
}
|
||||
|
|
|
@ -1,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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div onClick={this.toggleMenu} className={classnames(`dropdown ${className}`, {open: isOpen})}>
|
||||
<div className={`btn dropdown-toggle ${buttonSize} ${buttonColor}`}>
|
||||
{iconName ? <span className={classnames('icon', {[iconName]: true})}></span> : null}
|
||||
<span className="dropdown-selected">{selected}</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{isOpen ?
|
||||
<ul className="dropdown-menu" style={{width: menuWidth}}>
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<li className="dropdown-item" key={i}>
|
||||
<a href="#" onClick={() => this.handleSelection(item)}>
|
||||
{item.text}
|
||||
</a>
|
||||
{actions.length > 0 ?
|
||||
<div className="dropdown-item__actions">
|
||||
{actions.map((action) => {
|
||||
return (
|
||||
<button key={action.text} className="dropdown-item__action" onClick={(e) => this.handleAction(e, action, item)}>
|
||||
<span title={action.text} className={`icon ${action.icon}`}></span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{
|
||||
addNew ?
|
||||
<li>
|
||||
<Link to={addNew.url}>
|
||||
{addNew.text}
|
||||
</Link>
|
||||
</li> :
|
||||
null
|
||||
}
|
||||
</ul>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div onClick={this.toggleMenu} className={`dropdown ${className}`}>
|
||||
<div className="btn btn-sm btn-info dropdown-toggle">
|
||||
{iconName ? <span className={classnames("icon", {[iconName]: true})}></span> : null}
|
||||
<span className="dropdown-selected">{selected}</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
{self.state.isOpen ?
|
||||
<ul className="dropdown-menu show">
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<li className="dropdown-item" key={i}>
|
||||
<a href="#" onClick={() => self.handleSelection(item)}>
|
||||
{item.text}
|
||||
</a>
|
||||
<div className="dropdown-item__actions">
|
||||
{actions.map((action) => {
|
||||
return (
|
||||
<button key={action.text} data-target={action.target} data-toggle="modal" className="dropdown-item__action" onClick={(e) => self.handleAction(e, action, item)}>
|
||||
<span title={action.text} className={`icon ${action.icon}`}></span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -56,8 +56,6 @@ const SideNav = React.createClass({
|
|||
</NavBlock>
|
||||
<NavBlock icon="cog-thick" link={`${sourcePrefix}/manage-sources`}>
|
||||
<NavHeader link={`${sourcePrefix}/manage-sources`} title="Configuration" />
|
||||
<NavListItem link={`${sourcePrefix}/manage-sources`}>InfluxDB</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/kapacitor-config`}>Kapacitor</NavListItem>
|
||||
</NavBlock>
|
||||
{
|
||||
showLogout ? (
|
||||
|
|
|
@ -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 (
|
||||
<Link to={`/sources/${source.id}/kapacitors/new`}>Add Kapacitor</Link>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<Dropdown
|
||||
className="sources--kapacitor-selector"
|
||||
buttonColor="btn-info"
|
||||
buttonSize="btn-xs"
|
||||
items={kapacitorItems}
|
||||
onChoose={(item) => 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,
|
||||
}) => (
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">InfluxDB Sources</h2>
|
||||
<Link to={`/sources/${source.id}/manage-sources/new`} className="btn btn-sm btn-primary">Add Source</Link>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<table className="table v-center margin-bottom-zero">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Host</th>
|
||||
<th>Kapacitor</th>
|
||||
<th className="text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
sources.map((s) => {
|
||||
return (
|
||||
<tr key={s.id}>
|
||||
<td><Link to={`${location.pathname}/${s.id}/edit`}>{s.name}</Link> {s.default ? <span className="default-source-label">Default</span> : null}</td>
|
||||
<td className="monotype">{s.url}</td>
|
||||
<td>
|
||||
{
|
||||
kapacitorDropdown(s.kapacitors, s, router, setActiveKapacitor)
|
||||
}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<Link className="btn btn-success btn-xs" to={`/sources/${s.id}/hosts`}>Connect</Link>
|
||||
<button className="btn btn-danger btn-xs" onClick={() => handleDeleteSource(s)}><span className="icon trash"></span></button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
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)
|
|
@ -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 (
|
||||
<div className="page" id="manage-sources-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1>InfluxDB Sources</h1>
|
||||
<h1>Configuration</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{sourcesTitle}</h2>
|
||||
<Link to={`/sources/${this.props.source.id}/manage-sources/new`} className="btn btn-sm btn-primary">Add New Source</Link>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="table-responsive margin-bottom-zero">
|
||||
<table className="table v-center margin-bottom-zero">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Host</th>
|
||||
<th>Kapacitor</th>
|
||||
<th className="text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
sources.map((source) => {
|
||||
const kapacitorName = kapacitors[source.id] ? kapacitors[source.id].name : ''
|
||||
return (
|
||||
<tr key={source.id}>
|
||||
<td>{source.name}{source.default ? <span className="default-source-label">Default</span> : null}</td>
|
||||
<td className="monotype">{source.url}</td>
|
||||
<td>{kapacitorName ? kapacitorName : "--"}</td>
|
||||
<td className="text-right">
|
||||
<Link className="btn btn-info btn-xs" to={`${pathname}/${source.id}/edit`}><span className="icon pencil"></span></Link>
|
||||
<Link className="btn btn-success btn-xs" to={`/sources/${source.id}/hosts`}>Connect</Link>
|
||||
<button className="btn btn-danger btn-xs" onClick={() => this.handleDeleteSource(source)}><span className="icon trash"></span></button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfluxTable
|
||||
handleDeleteSource={this.handleDeleteSource}
|
||||
source={source}
|
||||
sources={sources}
|
||||
setActiveKapacitor={setActiveKapacitor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
|
@ -45,6 +45,8 @@
|
|||
@import 'components/query-maker';
|
||||
|
||||
// Pages
|
||||
|
||||
@import 'pages/config-endpoints';
|
||||
@import 'pages/signup';
|
||||
@import 'pages/auth-page';
|
||||
@import 'pages/kapacitor';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue