Merge pull request #1302 from influxdata/multiple-kapacitors

show multiple kapacitors
pull/1324/head
Jade McGough 2017-04-21 15:10:02 -07:00 committed by GitHub
commit 5ae6d238b1
48 changed files with 1614 additions and 1246 deletions

View File

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

View File

@ -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
}

View File

@ -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,
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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`

View File

@ -17,6 +17,7 @@ type postKapacitorRequest struct {
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
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)

View File

@ -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": {

View File

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

View File

@ -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} />

View File

@ -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',

View File

@ -1,184 +0,0 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
import AlertaConfig from './AlertaConfig'
import HipChatConfig from './HipChatConfig'
import OpsGenieConfig from './OpsGenieConfig'
import PagerDutyConfig from './PagerDutyConfig'
import SensuConfig from './SensuConfig'
import SlackConfig from './SlackConfig'
import SMTPConfig from './SMTPConfig'
import TalkConfig from './TalkConfig'
import TelegramConfig from './TelegramConfig'
import VictorOpsConfig from './VictorOpsConfig'
const AlertOutputs = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
kapacitor: PropTypes.shape({
url: PropTypes.string.isRequired,
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}),
addFlashMessage: PropTypes.func.isRequired,
},
getInitialState() {
return {
selectedEndpoint: 'smtp',
configSections: null,
}
},
componentDidMount() {
this.refreshKapacitorConfig(this.props.kapacitor)
},
componentWillReceiveProps(nextProps) {
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
this.refreshKapacitorConfig(nextProps.kapacitor)
}
},
refreshKapacitorConfig(kapacitor) {
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
this.setState({configSections: sections})
}).catch(() => {
this.setState({configSections: null})
this.props.addFlashMessage({type: 'error', text: `There was an error getting the Kapacitor config`})
})
},
getSection(sections, section) {
return _.get(sections, [section, 'elements', '0'], null)
},
handleSaveConfig(section, properties) {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: `There was an error saving the kapacitor config`})
})
}
},
changeSelectedEndpoint(e) {
this.setState({
selectedEndpoint: e.target.value,
})
},
handleTest(section, properties) {
const propsToSend = this.sanitizeProperties(section, properties)
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: `There was an error testing the slack alert`})
})
},
sanitizeProperties(section, properties) {
const cleanProps = Object.assign({}, properties, {enabled: true})
const {redacted} = this.getSection(this.state.configSections, section)
if (redacted && redacted.length) {
redacted.forEach((badProp) => {
if (properties[badProp] === 'true') {
delete cleanProps[badProp]
}
})
}
return cleanProps
},
render() {
const {configSections, selectedEndpoint} = this.state
if (!configSections) { // could use this state to conditionally render spinner or error message
return null
}
return (
<div className="panel panel-minimal">
<div className="panel-body">
<h4 className="text-center no-user-select">Configure Alert Endpoints</h4>
<br/>
<div className="row">
<div className="form-group col-xs-12 col-sm-8 col-sm-offset-2">
<label htmlFor="alert-endpoint" className="sr-only">Alert Enpoint</label>
<select value={this.state.selectedEndpoint} className="form-control" id="source" onChange={this.changeSelectedEndpoint}>
<option value="alerta">Alerta</option>
<option value="hipchat">HipChat</option>
<option value="opsgenie">OpsGenie</option>
<option value="pagerduty">PagerDuty</option>
<option value="sensu">Sensu</option>
<option value="slack">Slack</option>
<option value="smtp">SMTP</option>
<option value="talk">Talk</option>
<option value="telegram">Telegram</option>
<option value="victorops">VictorOps</option>
</select>
</div>
</div>
<div className="row">
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
<hr/>
</div>
<div className="col-xs-12 col-sm-8 col-sm-offset-2">
{this.renderAlertConfig(selectedEndpoint)}
</div>
</div>
</div>
</div>
)
},
renderAlertConfig(endpoint) {
const {configSections} = this.state
const save = (properties) => {
this.handleSaveConfig(endpoint, properties)
}
const test = (properties) => {
this.handleTest(endpoint, properties)
}
switch (endpoint) {
case 'alerta': {
return <AlertaConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'smtp': {
return <SMTPConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'slack': {
return <SlackConfig onSave={save} onTest={test} config={this.getSection(configSections, endpoint)} />
}
case 'victorops': {
return <VictorOpsConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'telegram': {
return <TelegramConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'opsgenie': {
return <OpsGenieConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'pagerduty': {
return <PagerDutyConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'hipchat': {
return <HipChatConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'sensu': {
return <SensuConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
case 'talk': {
return <TalkConfig onSave={save} config={this.getSection(configSections, endpoint)} />
}
}
},
})
export default AlertOutputs

View File

@ -0,0 +1,189 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {getKapacitorConfig, updateKapacitorConfigSection, testAlertOutput} from 'shared/apis'
import {
AlertaConfig,
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
SensuConfig,
SlackConfig,
SMTPConfig,
TalkConfig,
TelegramConfig,
VictorOpsConfig,
} from './config'
class AlertTabs extends Component {
constructor(props) {
super(props)
this.state = {
selectedEndpoint: 'smtp',
configSections: null,
}
this.refreshKapacitorConfig = ::this.refreshKapacitorConfig
this.getSection = ::this.getSection
this.handleSaveConfig = ::this.handleSaveConfig
this.handleTest = ::this.handleTest
this.sanitizeProperties = ::this.sanitizeProperties
}
componentDidMount() {
this.refreshKapacitorConfig(this.props.kapacitor)
}
componentWillReceiveProps(nextProps) {
if (this.props.kapacitor.url !== nextProps.kapacitor.url) {
this.refreshKapacitorConfig(nextProps.kapacitor)
}
}
refreshKapacitorConfig(kapacitor) {
getKapacitorConfig(kapacitor).then(({data: {sections}}) => {
this.setState({configSections: sections})
}).catch(() => {
this.setState({configSections: null})
this.props.addFlashMessage({type: 'error', text: 'There was an error getting the Kapacitor config'})
})
}
getSection(sections, section) {
return _.get(sections, [section, 'elements', '0'], null)
}
handleSaveConfig(section, properties) {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend).then(() => {
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({type: 'success', text: `Alert for ${section} successfully saved`})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: 'There was an error saving the kapacitor config'})
})
}
}
handleTest(section, properties) {
const propsToSend = this.sanitizeProperties(section, properties)
testAlertOutput(this.props.kapacitor, section, propsToSend).then(() => {
this.props.addFlashMessage({type: 'success', text: 'Slack test message sent'})
}).catch(() => {
this.props.addFlashMessage({type: 'error', text: 'There was an error testing the slack alert'})
})
}
sanitizeProperties(section, properties) {
const cleanProps = Object.assign({}, properties, {enabled: true})
const {redacted} = this.getSection(this.state.configSections, section)
if (redacted && redacted.length) {
redacted.forEach((badProp) => {
if (properties[badProp] === 'true') {
delete cleanProps[badProp]
}
})
}
return cleanProps
}
render() {
const {configSections} = this.state
if (!configSections) {
return null
}
const test = (properties) => {
this.handleTest('slack', properties)
}
const tabs = [
{
type: 'Alerta',
component: (<AlertaConfig onSave={(p) => this.handleSaveConfig('alerta', p)} config={this.getSection(configSections, 'alerta')} />),
},
{
type: 'SMTP',
component: (<SMTPConfig onSave={(p) => this.handleSaveConfig('smtp', p)} config={this.getSection(configSections, 'smtp')} />),
},
{
type: 'Slack',
component: (<SlackConfig onSave={(p) => this.handleSaveConfig('slack', p)} onTest={test} config={this.getSection(configSections, 'slack')} />),
},
{
type: 'VictorOps',
component: (<VictorOpsConfig onSave={(p) => this.handleSaveConfig('victorops', p)} config={this.getSection(configSections, 'victorops')} />),
},
{
type: 'Telegram',
component: (<TelegramConfig onSave={(p) => this.handleSaveConfig('telegram', p)} config={this.getSection(configSections, 'telegram')} />),
},
{
type: 'OpsGenie',
component: (<OpsGenieConfig onSave={(p) => this.handleSaveConfig('opsgenie', p)} config={this.getSection(configSections, 'opsgenie')} />),
},
{
type: 'PagerDuty',
component: (<PagerDutyConfig onSave={(p) => this.handleSaveConfig('pagerduty', p)} config={this.getSection(configSections, 'pagerduty')} />),
},
{
type: 'HipChat',
component: (<HipChatConfig onSave={(p) => this.handleSaveConfig('hipchat', p)} config={this.getSection(configSections, 'hipchat')} />),
},
{
type: 'Sensu',
component: (<SensuConfig onSave={(p) => this.handleSaveConfig('sensu', p)} config={this.getSection(configSections, 'sensu')} />),
},
{
type: 'Talk',
component: (<TalkConfig onSave={(p) => this.handleSaveConfig('talk', p)} config={this.getSection(configSections, 'talk')} />),
},
]
return (
<div>
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">Configure Alert Endpoints</h2>
</div>
</div>
<Tabs tabContentsClass="config-endpoint">
<TabList customClass="config-endpoint--tabs">
{
tabs.map((t, i) => (<Tab key={tabs[i].type}>{tabs[i].type}</Tab>))
}
</TabList>
<TabPanels customClass="config-endpoint--tab-contents">
{
tabs.map((t, i) => (<TabPanel key={tabs[i].type}>{t.component}</TabPanel>))
}
</TabPanels>
</Tabs>
</div>
)
}
}
const {
func,
shape,
string,
} = PropTypes
AlertTabs.propTypes = {
source: shape({
id: string.isRequired,
}).isRequired,
kapacitor: shape({
url: string.isRequired,
links: shape({
proxy: string.isRequired,
}).isRequired,
}),
addFlashMessage: func.isRequired,
}
export default AlertTabs

View File

@ -1,71 +0,0 @@
import React, {PropTypes} from 'react'
const AlertaConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
environment: PropTypes.string,
origin: PropTypes.string,
token: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
environment: this.environment.value,
origin: this.origin.value,
token: this.token.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {environment, origin, token, url} = this.props.config.options
return (
<div className="col-xs-12">
<h4 className="text-center no-user-select">Alerta Alert</h4>
<br/>
<form onSubmit={this.handleSaveAlert}>
<p className="no-user-select">
Have alerts sent to Alerta
</p>
<div className="form-group col-xs-12">
<label htmlFor="environment">Environment</label>
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="origin">Origin</label>
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">Token</label>
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
<span>Note: a value of <code>true</code> indicates the Alerta Token has been set</span>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">User</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default AlertaConfig

View File

@ -1,100 +0,0 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const HipchatConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
room: this.room.value,
url: `https://${this.url.value}.hipchat.com/v2/room`,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url, room, token} = options
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
return (
<div>
<h4 className="text-center no-user-select">HipChat Alert</h4>
<br/>
<p className="no-user-select">Send alert messages to HipChat.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">Subdomain</label>
<input
className="form-control"
id="url"
type="text"
placeholder="your-subdomain"
ref={(r) => this.url = r}
defaultValue={subdomain && subdomain.length ? subdomain : ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="room">Room</label>
<input
className="form-control"
id="room"
type="text"
placeholder="your-hipchat-room"
ref={(r) => this.room = r}
defaultValue={room || ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={HIPCHAT_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-hipchat-token"
ref={(r) => this.token = r}
defaultValue={token || ''}
/>
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default HipchatConfig

View File

@ -1,31 +1,9 @@
import React, {PropTypes} from 'react'
import 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

View File

@ -1,56 +0,0 @@
import React, {PropTypes} from 'react'
const PagerDutyConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'service-key': PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
serviceKey: this.serviceKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url} = options
const serviceKey = options['service-key']
return (
<div>
<h4 className="text-center no-user-select">PagerDuty Alert</h4>
<br/>
<p className="no-user-select">You can have alerts sent to PagerDuty by entering info below.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label>
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">PagerDuty URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default PagerDutyConfig

View File

@ -1,74 +0,0 @@
import React, {PropTypes} from 'react'
const SMTPConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
host: PropTypes.string,
port: PropTypes.number,
username: PropTypes.string,
password: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
host: this.host.value,
port: this.port.value,
from: this.from.value,
username: this.username.value,
password: this.password.value,
}
this.props.onSave(properties)
},
render() {
const {host, port, from, username, password} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">SMTP Alert</h4>
<br/>
<p className="no-user-select">You can have alerts sent to an email address by setting up an SMTP endpoint.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-host">SMTP Host</label>
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-port">SMTP Port</label>
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="smtp-from">From Email</label>
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-user">User</label>
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-password">Password</label>
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SMTPConfig

View File

@ -1,53 +0,0 @@
import React, {PropTypes} from 'react'
const SensuConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
source: PropTypes.string.isRequired,
addr: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
source: this.source.value,
addr: this.addr.value,
}
this.props.onSave(properties)
},
render() {
const {source, addr} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Sensu Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to Sensu.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="source">Source</label>
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="address">Address</label>
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SensuConfig

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react'
const SlackConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
url: PropTypes.bool.isRequired,
channel: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
onTest: PropTypes.func.isRequired,
},
getInitialState() {
return {
testEnabled: !!this.props.config.options.url,
}
},
componentWillReceiveProps(nextProps) {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
channel: this.channel.value,
}
this.props.onSave(properties)
},
handleTest(e) {
e.preventDefault()
this.props.onTest({
url: this.url.value,
channel: this.channel.value,
})
},
render() {
const {url, channel} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Slack Alert</h4>
<br/>
<p className="no-user-select">Post alerts to a Slack channel.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="slack-channel">Slack Channel (optional)</label>
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 text-center">
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
<button className="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default SlackConfig

View File

@ -1,61 +0,0 @@
import React, {PropTypes} from 'react'
const {
bool,
string,
shape,
func,
} = PropTypes
const TalkConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
author_name: this.author.value,
}
this.props.onSave(properties)
},
render() {
const {url, author_name: author} = this.props.config.options
return (
<div>
<h4 className="text-center no-user-select">Talk Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to Talk.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="author">Author Name</label>
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default TalkConfig

View File

@ -1,142 +0,0 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const TelegramConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
let parseMode
if (this.parseModeHTML.checked) {
parseMode = 'HTML'
}
if (this.parseModeMarkdown.checked) {
parseMode = 'Markdown'
}
const properties = {
'chat-id': this.chatID.value,
'disable-notification': this.disableNotification.checked,
'disable-web-page-preview': this.disableWebPagePreview.checked,
'parse-mode': parseMode,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {token} = options
const chatID = options['chat-id']
const disableNotification = options['disable-notification']
const disableWebPagePreview = options['disable-web-page-preview']
const parseMode = options['parse-mode']
return (
<div>
<h4 className="text-center no-user-select">Telegram Alert</h4>
<br/>
<p className="no-user-select">
Send alert messages to a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram bot</a>.
</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={TELEGRAM_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-telegram-token"
ref={(r) => this.token = r}
defaultValue={token || ''}>
</input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="chat-id">
Chat ID
<QuestionMarkTooltip
tipID="chat-id"
tipContent={TELEGRAM_CHAT_ID_TIP}
/>
</label>
<input
className="form-control"
id="chat-id"
type="text"
placeholder="your-telegram-chat-id"
ref={(r) => this.chatID = r}
defaultValue={chatID || ''}>
</input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="parseMode">Select the alert message format</label>
<div className="form-control-static">
<div className="radio">
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
<label htmlFor="parseModeMarkdown">Markdown</label>
</div>
<div className="radio">
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
<label htmlFor="parseModeHTML">HTML</label>
</div>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
<label htmlFor="disableWebPagePreview">
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
</label>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
<label htmlFor="disableNotification">
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
</label>
</div>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default TelegramConfig

View File

@ -1,64 +0,0 @@
import React, {PropTypes} from 'react'
const VictorOpsConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'api-key': PropTypes.bool,
'routing-key': PropTypes.string,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
'api-key': this.apiKey.value,
'routing-key': this.routingKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const apiKey = options['api-key']
const routingKey = options['routing-key']
const {url} = options
return (
<div>
<h4 className="text-center no-user-select">VictorOps Alert</h4>
<br/>
<p className="no-user-select">Have alerts sent to VictorOps.</p>
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="routing-key">Routing Key</label>
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">VictorOps URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
</div>
)
},
})
export default VictorOpsConfig

View File

@ -0,0 +1,63 @@
import React, {PropTypes} from 'react'
const AlertaConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
environment: PropTypes.string,
origin: PropTypes.string,
token: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
environment: this.environment.value,
origin: this.origin.value,
token: this.token.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {environment, origin, token, url} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="environment">Environment</label>
<input className="form-control" id="environment" type="text" ref={(r) => this.environment = r} defaultValue={environment || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="origin">Origin</label>
<input className="form-control" id="origin" type="text" ref={(r) => this.origin = r} defaultValue={origin || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">Token</label>
<input className="form-control" id="token" type="text" ref={(r) => this.token = r} defaultValue={token || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Alerta Token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">User</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default AlertaConfig

View File

@ -0,0 +1,95 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {HIPCHAT_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const HipchatConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
room: string.isRequired,
token: bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
room: this.room.value,
url: `https://${this.url.value}.hipchat.com/v2/room`,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url, room, token} = options
const subdomain = url.replace('https://', '').replace('.hipchat.com/v2/room', '')
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">Subdomain</label>
<input
className="form-control"
id="url"
type="text"
placeholder="your-subdomain"
ref={(r) => this.url = r}
defaultValue={subdomain && subdomain.length ? subdomain : ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="room">Room</label>
<input
className="form-control"
id="room"
type="text"
placeholder="your-hipchat-room"
ref={(r) => this.room = r}
defaultValue={room || ''}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={HIPCHAT_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-hipchat-token"
ref={(r) => this.token = r}
defaultValue={token || ''}
/>
<label className="form-helper">Note: a value of <code>true</code> indicates the HipChat token has been set</label>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default HipchatConfig

View File

@ -64,25 +64,20 @@ const OpsGenieConfig = React.createClass({
const {currentTeams, currentRecipients} = this.state
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>
)
},
})

View File

@ -0,0 +1,51 @@
import React, {PropTypes} from 'react'
const PagerDutyConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'service-key': PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
serviceKey: this.serviceKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {url} = options
const serviceKey = options['service-key']
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label>
<input className="form-control" id="service-key" type="text" ref={(r) => this.serviceKey = r} defaultValue={serviceKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the PagerDuty service key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">PagerDuty URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default PagerDutyConfig

View File

@ -0,0 +1,69 @@
import React, {PropTypes} from 'react'
const SMTPConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
host: PropTypes.string,
port: PropTypes.number,
username: PropTypes.string,
password: PropTypes.bool,
from: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
host: this.host.value,
port: this.port.value,
from: this.from.value,
username: this.username.value,
password: this.password.value,
}
this.props.onSave(properties)
},
render() {
const {host, port, from, username, password} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-host">SMTP Host</label>
<input className="form-control" id="smtp-host" type="text" ref={(r) => this.host = r} defaultValue={host || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-port">SMTP Port</label>
<input className="form-control" id="smtp-port" type="text" ref={(r) => this.port = r} defaultValue={port || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="smtp-from">From Email</label>
<input className="form-control" id="smtp-from" placeholder="email@domain.com" type="text" ref={(r) => this.from = r} defaultValue={from || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-user">User</label>
<input className="form-control" id="smtp-user" type="text" ref={(r) => this.username = r} defaultValue={username || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-password">Password</label>
<input className="form-control" id="smtp-password" type="password" ref={(r) => this.password = r} defaultValue={`${password}`}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SMTPConfig

View File

@ -0,0 +1,48 @@
import React, {PropTypes} from 'react'
const SensuConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
source: PropTypes.string.isRequired,
addr: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
source: this.source.value,
addr: this.addr.value,
}
this.props.onSave(properties)
},
render() {
const {source, addr} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="source">Source</label>
<input className="form-control" id="source" type="text" ref={(r) => this.source = r} defaultValue={source || ''}></input>
</div>
<div className="form-group col-xs-12 col-md-6">
<label htmlFor="address">Address</label>
<input className="form-control" id="address" type="text" ref={(r) => this.addr = r} defaultValue={addr || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SensuConfig

View File

@ -0,0 +1,71 @@
import React, {PropTypes} from 'react'
const SlackConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
url: PropTypes.bool.isRequired,
channel: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
onTest: PropTypes.func.isRequired,
},
getInitialState() {
return {
testEnabled: !!this.props.config.options.url,
}
},
componentWillReceiveProps(nextProps) {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
channel: this.channel.value,
}
this.props.onSave(properties)
},
handleTest(e) {
e.preventDefault()
this.props.onTest({
url: this.url.value,
channel: this.channel.value,
})
},
render() {
const {url, channel} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="slack-url">Slack Webhook URL (<a href="https://api.slack.com/incoming-webhooks" target="_">see more on Slack webhooks</a>)</label>
<input className="form-control" id="slack-url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Slack channel has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="slack-channel">Slack Channel (optional)</label>
<input className="form-control" id="slack-channel" type="text" placeholder="#alerts" ref={(r) => this.channel = r} defaultValue={channel || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 text-center">
<a className="btn btn-warning" onClick={this.handleTest} disabled={!this.state.testEnabled}>Send Test Message</a>
<button className="btn btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default SlackConfig

View File

@ -0,0 +1,56 @@
import React, {PropTypes} from 'react'
const {
bool,
string,
shape,
func,
} = PropTypes
const TalkConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
url: bool.isRequired,
author_name: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
url: this.url.value,
author_name: this.author.value,
}
this.props.onSave(properties)
},
render() {
const {url, author_name: author} = this.props.config.options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="url">URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates that the Talk URL has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="author">Author Name</label>
<input className="form-control" id="author" type="text" ref={(r) => this.author = r} defaultValue={author || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default TalkConfig

View File

@ -0,0 +1,138 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {TELEGRAM_CHAT_ID_TIP, TELEGRAM_TOKEN_TIP} from 'src/kapacitor/copy'
const {
bool,
func,
shape,
string,
} = PropTypes
const TelegramConfig = React.createClass({
propTypes: {
config: shape({
options: shape({
'chat-id': string.isRequired,
'disable-notification': bool.isRequired,
'disable-web-page-preview': bool.isRequired,
'parse-mode': string.isRequired,
token: bool.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
let parseMode
if (this.parseModeHTML.checked) {
parseMode = 'HTML'
}
if (this.parseModeMarkdown.checked) {
parseMode = 'Markdown'
}
const properties = {
'chat-id': this.chatID.value,
'disable-notification': this.disableNotification.checked,
'disable-web-page-preview': this.disableWebPagePreview.checked,
'parse-mode': parseMode,
token: this.token.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const {token} = options
const chatID = options['chat-id']
const disableNotification = options['disable-notification']
const disableWebPagePreview = options['disable-web-page-preview']
const parseMode = options['parse-mode']
return (
<form onSubmit={this.handleSaveAlert}>
<p className="no-user-select">
You need a <a href="https://docs.influxdata.com/kapacitor/v1.2/guides/event-handler-setup/#telegram-bot" target="_blank">Telegram Bot</a> to use this endpoint
</p>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={TELEGRAM_TOKEN_TIP}
/>
</label>
<input
className="form-control"
id="token"
type="text"
placeholder="your-telegram-token"
ref={(r) => this.token = r}
defaultValue={token || ''}>
</input>
<label className="form-helper">Note: a value of <code>true</code> indicates the Telegram token has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="chat-id">
Chat ID
<QuestionMarkTooltip
tipID="chat-id"
tipContent={TELEGRAM_CHAT_ID_TIP}
/>
</label>
<input
className="form-control"
id="chat-id"
type="text"
placeholder="your-telegram-chat-id"
ref={(r) => this.chatID = r}
defaultValue={chatID || ''}>
</input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="parseMode">Select the alert message format</label>
<div className="form-control-static">
<div className="radio">
<input id="parseModeMarkdown" type="radio" name="parseMode" value="markdown" defaultChecked={parseMode !== 'HTML'} ref={(r) => this.parseModeMarkdown = r} />
<label htmlFor="parseModeMarkdown">Markdown</label>
</div>
<div className="radio">
<input id="parseModeHTML" type="radio" name="parseMode" value="html" defaultChecked={parseMode === 'HTML'} ref={(r) => this.parseModeHTML = r} />
<label htmlFor="parseModeHTML">HTML</label>
</div>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableWebPagePreview" type="checkbox" defaultChecked={disableWebPagePreview} ref={(r) => this.disableWebPagePreview = r} />
<label htmlFor="disableWebPagePreview">
Disable <a href="https://telegram.org/blog/link-preview" target="_blank">link previews</a> in alert messages.
</label>
</div>
</div>
<div className="form-group col-xs-12">
<div className="form-control-static">
<input id="disableNotification" type="checkbox" defaultChecked={disableNotification} ref={(r) => this.disableNotification = r} />
<label htmlFor="disableNotification">
Disable notifications on iOS devices and disable sounds on Android devices. Android users continue to receive notifications.
</label>
</div>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default TelegramConfig

View File

@ -0,0 +1,59 @@
import React, {PropTypes} from 'react'
const VictorOpsConfig = React.createClass({
propTypes: {
config: PropTypes.shape({
options: PropTypes.shape({
'api-key': PropTypes.bool,
'routing-key': PropTypes.string,
url: PropTypes.string,
}).isRequired,
}).isRequired,
onSave: PropTypes.func.isRequired,
},
handleSaveAlert(e) {
e.preventDefault()
const properties = {
'api-key': this.apiKey.value,
'routing-key': this.routingKey.value,
url: this.url.value,
}
this.props.onSave(properties)
},
render() {
const {options} = this.props.config
const apiKey = options['api-key']
const routingKey = options['routing-key']
const {url} = options
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<input className="form-control" id="api-key" type="text" ref={(r) => this.apiKey = r} defaultValue={apiKey || ''}></input>
<label className="form-helper">Note: a value of <code>true</code> indicates the VictorOps API key has been set</label>
</div>
<div className="form-group col-xs-12">
<label htmlFor="routing-key">Routing Key</label>
<input className="form-control" id="routing-key" type="text" ref={(r) => this.routingKey = r} defaultValue={routingKey || ''}></input>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">VictorOps URL</label>
<input className="form-control" id="url" type="text" ref={(r) => this.url = r} defaultValue={url || ''}></input>
</div>
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
<button className="btn btn-block btn-primary" type="submit">Save</button>
</div>
</form>
)
},
})
export default VictorOpsConfig

View File

@ -0,0 +1,23 @@
import AlertaConfig from './AlertaConfig'
import HipChatConfig from './HipChatConfig'
import OpsGenieConfig from './OpsGenieConfig'
import PagerDutyConfig from './PagerDutyConfig'
import SensuConfig from './SensuConfig'
import SlackConfig from './SlackConfig'
import SMTPConfig from './SMTPConfig'
import TalkConfig from './TalkConfig'
import TelegramConfig from './TelegramConfig'
import VictorOpsConfig from './VictorOpsConfig'
export {
AlertaConfig,
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
SensuConfig,
SlackConfig,
SMTPConfig,
TalkConfig,
TelegramConfig,
VictorOpsConfig,
}

View File

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

View File

@ -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)

View File

@ -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)
}

View File

@ -1,4 +1,8 @@
import {deleteSource, getSources} from 'src/shared/apis'
import {deleteSource,
getSources,
getKapacitors as getKapacitorsAJAX,
updateKapacitor as updateKapacitorAJAX,
} from 'src/shared/apis'
import {publishNotification} from './notifications'
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)
}

View File

@ -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,
},
})
}

View File

@ -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)

View File

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

View File

@ -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 ? (

View File

@ -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)

View File

@ -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)

View File

@ -45,6 +45,8 @@
@import 'components/query-maker';
// Pages
@import 'pages/config-endpoints';
@import 'pages/signup';
@import 'pages/auth-page';
@import 'pages/kapacitor';

View File

@ -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;
}

View File

@ -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;
}
}