Merge branch 'master' into feature/pushover_support-1680
commit
200b24788b
|
@ -6,6 +6,7 @@
|
|||
### Features
|
||||
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
|
||||
1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards.
|
||||
1. [#1738](https://github.com/influxdata/chronograf/pull/1738): Add shared secret JWT authorization to InfluxDB
|
||||
1. [#1724](https://github.com/influxdata/chronograf/pull/1724): Add Pushover alert support
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ func MarshalSource(s chronograf.Source) ([]byte, error) {
|
|||
Type: s.Type,
|
||||
Username: s.Username,
|
||||
Password: s.Password,
|
||||
SharedSecret: s.SharedSecret,
|
||||
URL: s.URL,
|
||||
MetaURL: s.MetaURL,
|
||||
InsecureSkipVerify: s.InsecureSkipVerify,
|
||||
|
@ -37,6 +38,7 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error {
|
|||
s.Type = pb.Type
|
||||
s.Username = pb.Username
|
||||
s.Password = pb.Password
|
||||
s.SharedSecret = pb.SharedSecret
|
||||
s.URL = pb.URL
|
||||
s.MetaURL = pb.MetaURL
|
||||
s.InsecureSkipVerify = pb.InsecureSkipVerify
|
||||
|
|
|
@ -51,6 +51,7 @@ type Source struct {
|
|||
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"`
|
||||
InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"`
|
||||
MetaURL string `protobuf:"bytes,10,opt,name=MetaURL,proto3" json:"MetaURL,omitempty"`
|
||||
SharedSecret string `protobuf:"bytes,11,opt,name=SharedSecret,proto3" json:"SharedSecret,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Source) Reset() { *m = Source{} }
|
||||
|
@ -293,59 +294,60 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 858 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
|
||||
0x14, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0xee, 0x16, 0x34, 0x5a, 0xb1, 0x06, 0x6e, 0x22, 0x0b, 0xa4,
|
||||
0x82, 0x44, 0x41, 0xec, 0x13, 0xb4, 0xb5, 0x84, 0x42, 0xbb, 0x4b, 0x99, 0xb4, 0xe5, 0x0a, 0xad,
|
||||
0x26, 0xc9, 0x49, 0x6b, 0xed, 0x24, 0x36, 0x63, 0xbb, 0x59, 0xbf, 0x02, 0x57, 0x3c, 0x01, 0x12,
|
||||
0x12, 0x57, 0x5c, 0xf2, 0x02, 0x3c, 0x04, 0x2f, 0x84, 0xce, 0xcc, 0xf8, 0x27, 0x6c, 0x41, 0x7b,
|
||||
0xb5, 0x77, 0xf3, 0x9d, 0x33, 0xf9, 0xe6, 0xfc, 0x7c, 0x9f, 0x03, 0x87, 0xd9, 0xb6, 0x42, 0xbd,
|
||||
0x95, 0xea, 0xb8, 0xd0, 0x79, 0x95, 0xf3, 0xb0, 0xc5, 0xc9, 0xcf, 0x23, 0x18, 0xcf, 0xf3, 0x5a,
|
||||
0x2f, 0x91, 0x1f, 0xc2, 0x68, 0x96, 0xc6, 0x6c, 0xca, 0x8e, 0x3c, 0x31, 0x9a, 0xa5, 0x9c, 0x83,
|
||||
0xff, 0x42, 0x6e, 0x30, 0x1e, 0x4d, 0xd9, 0x51, 0x24, 0xcc, 0x99, 0x62, 0x57, 0x4d, 0x81, 0xb1,
|
||||
0x67, 0x63, 0x74, 0xe6, 0x1f, 0x41, 0x78, 0x5d, 0x12, 0xdb, 0x06, 0x63, 0xdf, 0xc4, 0x3b, 0x4c,
|
||||
0xb9, 0x4b, 0x59, 0x96, 0xbb, 0x5c, 0xaf, 0xe2, 0xc0, 0xe6, 0x5a, 0xcc, 0xdf, 0x07, 0xef, 0x5a,
|
||||
0x5c, 0xc4, 0x63, 0x13, 0xa6, 0x23, 0x8f, 0x61, 0x92, 0xe2, 0x5a, 0xd6, 0xaa, 0x8a, 0x27, 0x53,
|
||||
0x76, 0x14, 0x8a, 0x16, 0x12, 0xcf, 0x15, 0x2a, 0xbc, 0xd5, 0x72, 0x1d, 0x87, 0x96, 0xa7, 0xc5,
|
||||
0xfc, 0x18, 0xf8, 0x6c, 0x5b, 0xe2, 0xb2, 0xd6, 0x38, 0x7f, 0x95, 0x15, 0x37, 0xa8, 0xb3, 0x75,
|
||||
0x13, 0x47, 0x86, 0xe0, 0x81, 0x0c, 0xbd, 0xf2, 0x1c, 0x2b, 0x49, 0x6f, 0x83, 0xa1, 0x6a, 0x61,
|
||||
0xf2, 0x0b, 0x83, 0x28, 0x95, 0xe5, 0xdd, 0x22, 0x97, 0x7a, 0xf5, 0x56, 0xf3, 0xf8, 0x02, 0x82,
|
||||
0x25, 0x2a, 0x55, 0xc6, 0xde, 0xd4, 0x3b, 0x3a, 0xf8, 0xfa, 0xe9, 0x71, 0x37, 0xe8, 0x8e, 0xe7,
|
||||
0x0c, 0x95, 0x12, 0xf6, 0x16, 0xff, 0x0a, 0xa2, 0x0a, 0x37, 0x85, 0x92, 0x15, 0x96, 0xb1, 0x6f,
|
||||
0x7e, 0xc2, 0xfb, 0x9f, 0x5c, 0xb9, 0x94, 0xe8, 0x2f, 0x25, 0x7f, 0x30, 0x78, 0xbc, 0x47, 0xc5,
|
||||
0x1f, 0x01, 0x7b, 0x6d, 0xaa, 0x0a, 0x04, 0x7b, 0x4d, 0xa8, 0x31, 0x15, 0x05, 0x82, 0x35, 0x84,
|
||||
0x76, 0x66, 0x37, 0x81, 0x60, 0x3b, 0x42, 0x77, 0x66, 0x23, 0x81, 0x60, 0x77, 0xfc, 0x33, 0x98,
|
||||
0xfc, 0x54, 0xa3, 0xce, 0xb0, 0x8c, 0x03, 0xf3, 0xf2, 0x7b, 0xfd, 0xcb, 0xdf, 0xd7, 0xa8, 0x1b,
|
||||
0xd1, 0xe6, 0xa9, 0x53, 0xb3, 0x4d, 0xbb, 0x1a, 0x73, 0xa6, 0x58, 0x45, 0x9b, 0x9f, 0xd8, 0x18,
|
||||
0x9d, 0xdd, 0x84, 0xec, 0x3e, 0x46, 0xb3, 0x34, 0xf9, 0x8b, 0xd1, 0x9a, 0x6c, 0xe9, 0x83, 0xf1,
|
||||
0x99, 0x24, 0xff, 0x10, 0x42, 0x6a, 0xeb, 0xe5, 0xbd, 0xd4, 0x6e, 0x84, 0x13, 0xc2, 0x37, 0x52,
|
||||
0xf3, 0x2f, 0x61, 0x7c, 0x2f, 0x55, 0x8d, 0x0f, 0x8c, 0xb1, 0xa5, 0xbb, 0xa1, 0xbc, 0x70, 0xd7,
|
||||
0xba, 0x62, 0xfc, 0x41, 0x31, 0x4f, 0x20, 0x50, 0x72, 0x81, 0xca, 0xe9, 0xcc, 0x02, 0x5a, 0x10,
|
||||
0x75, 0xd5, 0x98, 0x5e, 0x1e, 0x64, 0xb6, 0xbd, 0xdb, 0x5b, 0xc9, 0x35, 0x3c, 0xde, 0x7b, 0xb1,
|
||||
0x7b, 0x89, 0xed, 0xbf, 0x64, 0xea, 0x70, 0x6d, 0x58, 0x40, 0x12, 0x2d, 0x51, 0xe1, 0xb2, 0xc2,
|
||||
0x95, 0x59, 0x41, 0x28, 0x3a, 0x9c, 0xfc, 0xc6, 0x7a, 0x5e, 0xf3, 0x1e, 0x89, 0x70, 0x99, 0x6f,
|
||||
0x36, 0x72, 0xbb, 0x72, 0xd4, 0x2d, 0xa4, 0xb9, 0xad, 0x16, 0x8e, 0x7a, 0xb4, 0x5a, 0x10, 0xd6,
|
||||
0x85, 0x33, 0xdc, 0x48, 0x17, 0x7c, 0x0a, 0x07, 0x1b, 0x94, 0x65, 0xad, 0x71, 0x83, 0xdb, 0xca,
|
||||
0x8d, 0x60, 0x18, 0xe2, 0x4f, 0x61, 0x52, 0xc9, 0xdb, 0x97, 0xaf, 0xb0, 0x71, 0xb3, 0x18, 0x57,
|
||||
0xf2, 0xf6, 0x1c, 0x1b, 0xfe, 0x31, 0x44, 0xeb, 0x0c, 0xd5, 0xca, 0xa4, 0xec, 0x72, 0x43, 0x13,
|
||||
0x38, 0xc7, 0x26, 0xf9, 0x9d, 0xc1, 0x78, 0x8e, 0xfa, 0x1e, 0xf5, 0x5b, 0x29, 0x7f, 0xe8, 0x7a,
|
||||
0xef, 0x7f, 0x5c, 0xef, 0x3f, 0xec, 0xfa, 0xa0, 0x77, 0xfd, 0x13, 0x08, 0xe6, 0x7a, 0x39, 0x4b,
|
||||
0x4d, 0x45, 0x9e, 0xb0, 0x80, 0x7f, 0x00, 0xe3, 0x93, 0x65, 0x95, 0xdd, 0xa3, 0xfb, 0x14, 0x38,
|
||||
0x94, 0xfc, 0xca, 0x60, 0x7c, 0x21, 0x9b, 0xbc, 0xae, 0xde, 0x50, 0xd8, 0x14, 0x0e, 0x4e, 0x8a,
|
||||
0x42, 0x65, 0x4b, 0x59, 0x65, 0xf9, 0xd6, 0x55, 0x3b, 0x0c, 0xd1, 0x8d, 0xe7, 0x83, 0xd9, 0xd9,
|
||||
0xba, 0x87, 0x21, 0xfe, 0x09, 0x04, 0x67, 0xc6, 0xd0, 0xd6, 0x9d, 0x87, 0xbd, 0x5e, 0xac, 0x8f,
|
||||
0x4d, 0x92, 0x1a, 0x3c, 0xa9, 0xab, 0x7c, 0xad, 0xf2, 0x9d, 0xe9, 0x24, 0x14, 0x1d, 0x4e, 0xfe,
|
||||
0x66, 0xe0, 0xbf, 0x2b, 0xa3, 0x3e, 0x02, 0x96, 0xb9, 0x45, 0xb2, 0xac, 0xb3, 0xed, 0x64, 0x60,
|
||||
0xdb, 0x18, 0x26, 0x8d, 0x96, 0xdb, 0x5b, 0x2c, 0xe3, 0x70, 0xea, 0x1d, 0x79, 0xa2, 0x85, 0x26,
|
||||
0x63, 0x3c, 0x52, 0xc6, 0xd1, 0xd4, 0x23, 0x05, 0x3a, 0xd8, 0x69, 0x1e, 0x7a, 0xcd, 0x27, 0x7f,
|
||||
0x32, 0x08, 0x3a, 0xe5, 0x9e, 0xed, 0x2b, 0xf7, 0xac, 0x57, 0x6e, 0x7a, 0xda, 0x2a, 0x37, 0x3d,
|
||||
0x25, 0x2c, 0x2e, 0x5b, 0xe5, 0x8a, 0x4b, 0x9a, 0xda, 0x37, 0x3a, 0xaf, 0x8b, 0xd3, 0xc6, 0x8e,
|
||||
0x37, 0x12, 0x1d, 0xa6, 0x75, 0xff, 0x70, 0x87, 0xda, 0xf5, 0x1c, 0x09, 0x87, 0x48, 0x1c, 0x17,
|
||||
0xc6, 0xd5, 0xb6, 0x4b, 0x0b, 0xf8, 0xa7, 0x10, 0x08, 0xea, 0xc2, 0xb4, 0xba, 0x37, 0x20, 0x13,
|
||||
0x16, 0x36, 0x9b, 0x3c, 0x73, 0xd7, 0x88, 0xe5, 0xba, 0x28, 0x50, 0x3b, 0x4d, 0x5b, 0x60, 0xb8,
|
||||
0xf3, 0x1d, 0xda, 0xcf, 0x91, 0x27, 0x2c, 0x48, 0x7e, 0x84, 0xe8, 0x44, 0xa1, 0xae, 0x44, 0xad,
|
||||
0xde, 0xfc, 0x88, 0x71, 0xf0, 0xbf, 0x9d, 0x7f, 0xf7, 0xa2, 0x75, 0x02, 0x9d, 0x7b, 0xfd, 0x7a,
|
||||
0xff, 0xd2, 0xef, 0xb9, 0x2c, 0xe4, 0x2c, 0x35, 0x8b, 0xf5, 0x84, 0x43, 0xc9, 0xe7, 0xe0, 0x93,
|
||||
0x4f, 0x06, 0xcc, 0xfe, 0x7f, 0x79, 0x6c, 0x31, 0x36, 0xff, 0xd6, 0xcf, 0xfe, 0x09, 0x00, 0x00,
|
||||
0xff, 0xff, 0xa7, 0xc6, 0x53, 0x22, 0xbf, 0x07, 0x00, 0x00,
|
||||
// 876 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x55, 0xdf, 0x6e, 0xe3, 0xc4,
|
||||
0x17, 0xd6, 0xc4, 0x76, 0x62, 0x9f, 0x76, 0xfb, 0xfb, 0x69, 0xb4, 0x62, 0x0d, 0xdc, 0x44, 0x16,
|
||||
0x48, 0x01, 0x89, 0x82, 0xd8, 0x27, 0x68, 0x6b, 0x09, 0x85, 0x76, 0x97, 0x32, 0x69, 0xcb, 0x15,
|
||||
0x5a, 0x4d, 0x9c, 0x93, 0xc6, 0x5a, 0x27, 0x36, 0x63, 0xbb, 0x59, 0xbf, 0x05, 0x4f, 0x80, 0x84,
|
||||
0xc4, 0x15, 0x17, 0x5c, 0xf0, 0x02, 0x3c, 0x04, 0x2f, 0x84, 0xce, 0xcc, 0xf8, 0x4f, 0xd8, 0x82,
|
||||
0xf6, 0x8a, 0xbb, 0xf9, 0xce, 0x19, 0x7f, 0xe7, 0xdf, 0x77, 0xc6, 0x70, 0x92, 0xee, 0x2a, 0x54,
|
||||
0x3b, 0x99, 0x9d, 0x16, 0x2a, 0xaf, 0x72, 0xee, 0xb7, 0x38, 0xfa, 0x6d, 0x04, 0xe3, 0x45, 0x5e,
|
||||
0xab, 0x04, 0xf9, 0x09, 0x8c, 0xe6, 0x71, 0xc8, 0xa6, 0x6c, 0xe6, 0x88, 0xd1, 0x3c, 0xe6, 0x1c,
|
||||
0xdc, 0x97, 0x72, 0x8b, 0xe1, 0x68, 0xca, 0x66, 0x81, 0xd0, 0x67, 0xb2, 0xdd, 0x34, 0x05, 0x86,
|
||||
0x8e, 0xb1, 0xd1, 0x99, 0x7f, 0x00, 0xfe, 0x6d, 0x49, 0x6c, 0x5b, 0x0c, 0x5d, 0x6d, 0xef, 0x30,
|
||||
0xf9, 0xae, 0x65, 0x59, 0xee, 0x73, 0xb5, 0x0a, 0x3d, 0xe3, 0x6b, 0x31, 0xff, 0x3f, 0x38, 0xb7,
|
||||
0xe2, 0x2a, 0x1c, 0x6b, 0x33, 0x1d, 0x79, 0x08, 0x93, 0x18, 0xd7, 0xb2, 0xce, 0xaa, 0x70, 0x32,
|
||||
0x65, 0x33, 0x5f, 0xb4, 0x90, 0x78, 0x6e, 0x30, 0xc3, 0x7b, 0x25, 0xd7, 0xa1, 0x6f, 0x78, 0x5a,
|
||||
0xcc, 0x4f, 0x81, 0xcf, 0x77, 0x25, 0x26, 0xb5, 0xc2, 0xc5, 0xeb, 0xb4, 0xb8, 0x43, 0x95, 0xae,
|
||||
0x9b, 0x30, 0xd0, 0x04, 0x8f, 0x78, 0x28, 0xca, 0x0b, 0xac, 0x24, 0xc5, 0x06, 0x4d, 0xd5, 0x42,
|
||||
0x1e, 0xc1, 0xf1, 0x62, 0x23, 0x15, 0xae, 0x16, 0x98, 0x28, 0xac, 0xc2, 0x23, 0xed, 0x3e, 0xb0,
|
||||
0x45, 0x3f, 0x32, 0x08, 0x62, 0x59, 0x6e, 0x96, 0xb9, 0x54, 0xab, 0x77, 0xea, 0xd9, 0x67, 0xe0,
|
||||
0x25, 0x98, 0x65, 0x65, 0xe8, 0x4c, 0x9d, 0xd9, 0xd1, 0x97, 0xcf, 0x4e, 0xbb, 0x61, 0x74, 0x3c,
|
||||
0x17, 0x98, 0x65, 0xc2, 0xdc, 0xe2, 0x5f, 0x40, 0x50, 0xe1, 0xb6, 0xc8, 0x64, 0x85, 0x65, 0xe8,
|
||||
0xea, 0x4f, 0x78, 0xff, 0xc9, 0x8d, 0x75, 0x89, 0xfe, 0x52, 0xf4, 0x2b, 0x83, 0x27, 0x07, 0x54,
|
||||
0xfc, 0x18, 0xd8, 0x1b, 0x9d, 0x95, 0x27, 0xd8, 0x1b, 0x42, 0x8d, 0xce, 0xc8, 0x13, 0xac, 0x21,
|
||||
0xb4, 0xd7, 0xf3, 0xf3, 0x04, 0xdb, 0x13, 0xda, 0xe8, 0xa9, 0x79, 0x82, 0x6d, 0xf8, 0x27, 0x30,
|
||||
0xf9, 0xa1, 0x46, 0x95, 0x62, 0x19, 0x7a, 0x3a, 0xf2, 0xff, 0xfa, 0xc8, 0xdf, 0xd6, 0xa8, 0x1a,
|
||||
0xd1, 0xfa, 0xa9, 0x52, 0x3d, 0x71, 0x33, 0x3e, 0x7d, 0x26, 0x5b, 0x45, 0xea, 0x98, 0x18, 0x1b,
|
||||
0x9d, 0x6d, 0x87, 0xcc, 0xcc, 0x46, 0xf3, 0x38, 0xfa, 0x83, 0xd1, 0x28, 0x4d, 0xea, 0x83, 0xf6,
|
||||
0x69, 0x27, 0x7f, 0x1f, 0x7c, 0x2a, 0xeb, 0xd5, 0x83, 0x54, 0xb6, 0x85, 0x13, 0xc2, 0x77, 0x52,
|
||||
0xf1, 0xcf, 0x61, 0xfc, 0x20, 0xb3, 0x1a, 0x1f, 0x69, 0x63, 0x4b, 0x77, 0x47, 0x7e, 0x61, 0xaf,
|
||||
0x75, 0xc9, 0xb8, 0x83, 0x64, 0x9e, 0x82, 0x97, 0xc9, 0x25, 0x66, 0x56, 0x8b, 0x06, 0xd0, 0x80,
|
||||
0xa8, 0xaa, 0x46, 0xd7, 0xf2, 0x28, 0xb3, 0xa9, 0xdd, 0xdc, 0x8a, 0x6e, 0xe1, 0xc9, 0x41, 0xc4,
|
||||
0x2e, 0x12, 0x3b, 0x8c, 0xa4, 0xf3, 0xb0, 0x65, 0x18, 0x40, 0x32, 0x2e, 0x31, 0xc3, 0xa4, 0xc2,
|
||||
0x95, 0x1e, 0x81, 0x2f, 0x3a, 0x1c, 0xfd, 0xcc, 0x7a, 0x5e, 0x1d, 0x8f, 0x84, 0x9a, 0xe4, 0xdb,
|
||||
0xad, 0xdc, 0xad, 0x2c, 0x75, 0x0b, 0xa9, 0x6f, 0xab, 0xa5, 0xa5, 0x1e, 0xad, 0x96, 0x84, 0x55,
|
||||
0x61, 0x97, 0x72, 0xa4, 0x0a, 0x3e, 0x85, 0xa3, 0x2d, 0xca, 0xb2, 0x56, 0xb8, 0xc5, 0x5d, 0x65,
|
||||
0x5b, 0x30, 0x34, 0xf1, 0x67, 0x30, 0xa9, 0xe4, 0xfd, 0xab, 0xd7, 0xd8, 0xd8, 0x5e, 0x8c, 0x2b,
|
||||
0x79, 0x7f, 0x89, 0x0d, 0xff, 0x10, 0x82, 0x75, 0x8a, 0xd9, 0x4a, 0xbb, 0xcc, 0x70, 0x7d, 0x6d,
|
||||
0xb8, 0xc4, 0x26, 0xfa, 0x85, 0xc1, 0x78, 0x81, 0xea, 0x01, 0xd5, 0x3b, 0x29, 0x7f, 0xf8, 0x32,
|
||||
0x38, 0xff, 0xf2, 0x32, 0xb8, 0x8f, 0xbf, 0x0c, 0x5e, 0xff, 0x32, 0x3c, 0x05, 0x6f, 0xa1, 0x92,
|
||||
0x79, 0xac, 0x33, 0x72, 0x84, 0x01, 0xfc, 0x3d, 0x18, 0x9f, 0x25, 0x55, 0xfa, 0x80, 0xf6, 0xb9,
|
||||
0xb0, 0x28, 0xfa, 0x89, 0xc1, 0xf8, 0x4a, 0x36, 0x79, 0x5d, 0xbd, 0xa5, 0xb0, 0x29, 0x1c, 0x9d,
|
||||
0x15, 0x45, 0x96, 0x26, 0xb2, 0x4a, 0xf3, 0x9d, 0xcd, 0x76, 0x68, 0xa2, 0x1b, 0x2f, 0x06, 0xbd,
|
||||
0x33, 0x79, 0x0f, 0x4d, 0xfc, 0x23, 0xf0, 0x2e, 0xf4, 0x42, 0x9b, 0xed, 0x3c, 0xe9, 0xf5, 0x62,
|
||||
0xf6, 0x58, 0x3b, 0xa9, 0xc0, 0xb3, 0xba, 0xca, 0xd7, 0x59, 0xbe, 0xd7, 0x95, 0xf8, 0xa2, 0xc3,
|
||||
0xd1, 0x9f, 0x0c, 0xdc, 0xff, 0x6a, 0x51, 0x8f, 0x81, 0xa5, 0x76, 0x90, 0x2c, 0xed, 0xd6, 0x76,
|
||||
0x32, 0x58, 0xdb, 0x10, 0x26, 0x8d, 0x92, 0xbb, 0x7b, 0x2c, 0x43, 0x7f, 0xea, 0xcc, 0x1c, 0xd1,
|
||||
0x42, 0xed, 0xd1, 0x3b, 0x52, 0x86, 0xc1, 0xd4, 0x21, 0x05, 0x5a, 0xd8, 0x69, 0x1e, 0x7a, 0xcd,
|
||||
0x47, 0xbf, 0x33, 0xf0, 0x3a, 0xe5, 0x5e, 0x1c, 0x2a, 0xf7, 0xa2, 0x57, 0x6e, 0x7c, 0xde, 0x2a,
|
||||
0x37, 0x3e, 0x27, 0x2c, 0xae, 0x5b, 0xe5, 0x8a, 0x6b, 0xea, 0xda, 0x57, 0x2a, 0xaf, 0x8b, 0xf3,
|
||||
0xc6, 0xb4, 0x37, 0x10, 0x1d, 0xa6, 0x71, 0x7f, 0xb7, 0x41, 0x65, 0x6b, 0x0e, 0x84, 0x45, 0x24,
|
||||
0x8e, 0x2b, 0xbd, 0xd5, 0xa6, 0x4a, 0x03, 0xf8, 0xc7, 0xe0, 0x09, 0xaa, 0x42, 0x97, 0x7a, 0xd0,
|
||||
0x20, 0x6d, 0x16, 0xc6, 0x1b, 0x3d, 0xb7, 0xd7, 0x88, 0xe5, 0xb6, 0x28, 0x50, 0x59, 0x4d, 0x1b,
|
||||
0xa0, 0xb9, 0xf3, 0x3d, 0x9a, 0xe7, 0xc8, 0x11, 0x06, 0x44, 0xdf, 0x43, 0x70, 0x96, 0xa1, 0xaa,
|
||||
0x44, 0x9d, 0xbd, 0xfd, 0x88, 0x71, 0x70, 0xbf, 0x5e, 0x7c, 0xf3, 0xb2, 0xdd, 0x04, 0x3a, 0xf7,
|
||||
0xfa, 0x75, 0xfe, 0xa6, 0xdf, 0x4b, 0x59, 0xc8, 0x79, 0xac, 0x07, 0xeb, 0x08, 0x8b, 0xa2, 0x4f,
|
||||
0xc1, 0xa5, 0x3d, 0x19, 0x30, 0xbb, 0xff, 0xb4, 0x63, 0xcb, 0xb1, 0xfe, 0xa3, 0x3f, 0xff, 0x2b,
|
||||
0x00, 0x00, 0xff, 0xff, 0x0f, 0x40, 0x84, 0xea, 0xe3, 0x07, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -2,23 +2,24 @@ syntax = "proto3";
|
|||
package internal;
|
||||
|
||||
message Source {
|
||||
int64 ID = 1; // ID is the unique ID of the source
|
||||
string Name = 2; // Name is the user-defined name for the source
|
||||
string Type = 3; // Type specifies which kinds of source (enterprise vs oss)
|
||||
string Username = 4; // Username is the username to connect to the source
|
||||
int64 ID = 1; // ID is the unique ID of the source
|
||||
string Name = 2; // Name is the user-defined name for the source
|
||||
string Type = 3; // Type specifies which kinds of source (enterprise vs oss)
|
||||
string Username = 4; // Username is the username to connect to the source
|
||||
string Password = 5;
|
||||
string URL = 6; // URL are the connections to the source
|
||||
bool Default = 7; // Flags an source as the default.
|
||||
string Telegraf = 8; // Telegraf is the db telegraf is written to. By default it is "telegraf"
|
||||
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the influx server
|
||||
string URL = 6; // URL are the connections to the source
|
||||
bool Default = 7; // Flags an source as the default.
|
||||
string Telegraf = 8; // Telegraf is the db telegraf is written to. By default it is "telegraf"
|
||||
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the influx server
|
||||
string MetaURL = 10; // MetaURL is the connection URL for the meta node.
|
||||
string SharedSecret = 11; // SharedSecret signs the optional InfluxDB JWT Authorization
|
||||
}
|
||||
|
||||
message Dashboard {
|
||||
int64 ID = 1; // ID is the unique ID of the dashboard
|
||||
string Name = 2; // Name is the user-defined name of the dashboard
|
||||
repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard
|
||||
repeated Template templates = 4; // Templates replace template variables within InfluxQL
|
||||
repeated Template templates = 4; // Templates replace template variables within InfluxQL
|
||||
}
|
||||
|
||||
message DashboardCell {
|
||||
|
|
|
@ -40,6 +40,38 @@ func TestMarshalSource(t *testing.T) {
|
|||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
}
|
||||
func TestMarshalSourceWithSecret(t *testing.T) {
|
||||
v := chronograf.Source{
|
||||
ID: 12,
|
||||
Name: "Fountain of Truth",
|
||||
Type: "influx",
|
||||
Username: "docbrown",
|
||||
SharedSecret: "hunter2s",
|
||||
URL: "http://twin-pines.mall.io:8086",
|
||||
MetaURL: "http://twin-pines.meta.io:8086",
|
||||
Default: true,
|
||||
Telegraf: "telegraf",
|
||||
}
|
||||
|
||||
var vv chronograf.Source
|
||||
if buf, err := internal.MarshalSource(v); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := internal.UnmarshalSource(buf, &vv); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(v, vv) {
|
||||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
|
||||
// Test if the new insecureskipverify works
|
||||
v.InsecureSkipVerify = true
|
||||
if buf, err := internal.MarshalSource(v); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := internal.UnmarshalSource(buf, &vv); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(v, vv) {
|
||||
t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalServer(t *testing.T) {
|
||||
v := chronograf.Server{
|
||||
|
|
|
@ -346,6 +346,7 @@ type Source struct {
|
|||
Type string `json:"type,omitempty"` // Type specifies which kinds of source (enterprise vs oss)
|
||||
Username string `json:"username,omitempty"` // Username is the username to connect to the source
|
||||
Password string `json:"password,omitempty"` // Password is in CLEARTEXT
|
||||
SharedSecret string `json:"sharedSecret,omitempty"` // ShareSecret is the optional signing secret for Influx JWT authorization
|
||||
URL string `json:"url"` // URL are the connections to the source
|
||||
MetaURL string `json:"metaUrl,omitempty"` // MetaURL is the url for the meta node
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the source is accepted.
|
||||
|
@ -625,44 +626,3 @@ type LayoutStore interface {
|
|||
// Update the dashboard in the store.
|
||||
Update(context.Context, Layout) error
|
||||
}
|
||||
|
||||
// SourceAndKapacitor is used to parse any NewSources server flag arguments
|
||||
type SourceAndKapacitor struct {
|
||||
Source Source `json:"influxdb"`
|
||||
Kapacitor Server `json:"kapacitor"`
|
||||
}
|
||||
|
||||
// NewSources adds sources to BoltDb idempotently by name, as well as respective kapacitors
|
||||
func NewSources(ctx context.Context, sourcesStore SourcesStore, serversStore ServersStore, srcsKaps []SourceAndKapacitor, logger Logger) error {
|
||||
srcs, err := sourcesStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SourceLoop:
|
||||
for _, srcKap := range srcsKaps {
|
||||
for _, src := range srcs {
|
||||
// If source already exists, do nothing
|
||||
if src.Name == srcKap.Source.Name {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", src.Name).
|
||||
Info("Source already exists")
|
||||
continue SourceLoop
|
||||
}
|
||||
}
|
||||
|
||||
src, err := sourcesStore.Add(ctx, srcKap.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcKap.Kapacitor.SrcID = src.ID
|
||||
_, err = serversStore.Add(ctx, srcKap.Kapacitor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
package chronograf_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func Test_NewSources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srcsKaps := []chronograf.SourceAndKapacitor{
|
||||
{
|
||||
Source: chronograf.Source{
|
||||
Default: true,
|
||||
InsecureSkipVerify: false,
|
||||
MetaURL: "http://metaurl.com",
|
||||
Name: "Influx 1",
|
||||
Password: "pass1",
|
||||
Telegraf: "telegraf",
|
||||
URL: "http://localhost:8086",
|
||||
Username: "user1",
|
||||
},
|
||||
Kapacitor: chronograf.Server{
|
||||
Active: true,
|
||||
Name: "Kapa 1",
|
||||
URL: "http://localhost:9092",
|
||||
},
|
||||
},
|
||||
}
|
||||
saboteurSrcsKaps := []chronograf.SourceAndKapacitor{
|
||||
{
|
||||
Source: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
Kapacitor: chronograf.Server{
|
||||
Name: "Kapa Aspiring Saboteur",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
srcs := []chronograf.Source{}
|
||||
srcsStore := mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return srcs, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcs = append(srcs, src)
|
||||
return src, nil
|
||||
},
|
||||
}
|
||||
srvs := []chronograf.Server{}
|
||||
srvsStore := mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvs = append(srvs, srv)
|
||||
return srv, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := chronograf.NewSources(ctx, &srcsStore, &srvsStore, srcsKaps, &mocks.TestLogger{})
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when creating New Sources. Error:", err)
|
||||
}
|
||||
if len(srcs) != 1 {
|
||||
t.Error("Expected one source in sourcesStore")
|
||||
}
|
||||
if len(srvs) != 1 {
|
||||
t.Error("Expected one source in serversStore")
|
||||
}
|
||||
|
||||
err = chronograf.NewSources(ctx, &srcsStore, &srvsStore, saboteurSrcsKaps, &mocks.TestLogger{})
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error when creating New Sources. Error:", err)
|
||||
}
|
||||
if len(srcs) != 1 {
|
||||
t.Error("Expected one source in sourcesStore")
|
||||
}
|
||||
if len(srvs) != 1 {
|
||||
t.Error("Expected one source in serversStore")
|
||||
}
|
||||
if !reflect.DeepEqual(srcs[0], srcsKaps[0].Source) {
|
||||
t.Error("Expected source in sourceStore to remain unchanged")
|
||||
}
|
||||
}
|
|
@ -120,13 +120,17 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
|||
|
||||
c.dataNodes = ring.New(len(cluster.DataNodes))
|
||||
for _, dn := range cluster.DataNodes {
|
||||
cl, err := influx.NewClient(dn.HTTPAddr, c.Logger)
|
||||
if err != nil {
|
||||
continue
|
||||
} else {
|
||||
c.dataNodes.Value = cl
|
||||
c.dataNodes = c.dataNodes.Next()
|
||||
cl := &influx.Client{
|
||||
Logger: c.Logger,
|
||||
}
|
||||
dataSrc := &chronograf.Source{}
|
||||
*dataSrc = *src
|
||||
dataSrc.URL = dn.HTTPAddr
|
||||
if err := cl.Connect(ctx, dataSrc); err != nil {
|
||||
continue
|
||||
}
|
||||
c.dataNodes.Value = cl
|
||||
c.dataNodes = c.dataNodes.Next()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -28,25 +28,9 @@ var (
|
|||
// Client is a device for retrieving time series data from an InfluxDB instance
|
||||
type Client struct {
|
||||
URL *url.URL
|
||||
Bearer Bearer
|
||||
InsecureSkipVerify bool
|
||||
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewClient initializes an HTTP Client for InfluxDB. UDP, although supported
|
||||
// for querying InfluxDB, is not supported here to remove the need to
|
||||
// explicitly Close the client.
|
||||
func NewClient(host string, lg chronograf.Logger) (*Client, error) {
|
||||
l := lg.WithField("host", host)
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
l.Error("Error initialize influx client: err:", err)
|
||||
return nil, err
|
||||
}
|
||||
return &Client{
|
||||
URL: u,
|
||||
Logger: l,
|
||||
}, nil
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// Response is a partial JSON decoded InfluxQL response used
|
||||
|
@ -88,6 +72,15 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
|||
params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
if c.Bearer != nil && u.User != nil {
|
||||
token, err := c.Bearer.Token(u.User.Username())
|
||||
if err != nil {
|
||||
logs.Error("Error creating token", err)
|
||||
return nil, fmt.Errorf("Unable to create token")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
hc := &http.Client{}
|
||||
if c.InsecureSkipVerify {
|
||||
hc.Transport = skipVerifyTransport
|
||||
|
@ -157,7 +150,7 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp
|
|||
}
|
||||
}
|
||||
|
||||
// Connect caches the URL for the data source
|
||||
// Connect caches the URL and optional Bearer Authorization for the data source
|
||||
func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
||||
u, err := url.Parse(src.URL)
|
||||
if err != nil {
|
||||
|
@ -169,6 +162,16 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
|||
c.InsecureSkipVerify = src.InsecureSkipVerify
|
||||
}
|
||||
c.URL = u
|
||||
|
||||
// Optionally, add the shared secret JWT token creation
|
||||
if src.Username != "" && src.SharedSecret != "" {
|
||||
c.Bearer = &BearerJWT{
|
||||
src.SharedSecret,
|
||||
}
|
||||
} else {
|
||||
// Clear out the bearer if not needed
|
||||
c.Bearer = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2,16 +2,34 @@ package influx_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gojwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
)
|
||||
|
||||
// NewClient initializes an HTTP Client for InfluxDB.
|
||||
func NewClient(host string, lg chronograf.Logger) (*influx.Client, error) {
|
||||
l := lg.WithField("host", host)
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
l.Error("Error initialize influx client: err:", err)
|
||||
return nil, err
|
||||
}
|
||||
return &influx.Client{
|
||||
URL: u,
|
||||
Logger: l,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
called := false
|
||||
|
@ -26,7 +44,7 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
|||
defer ts.Close()
|
||||
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -44,6 +62,126 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type MockBearer struct {
|
||||
Bearer string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockBearer) Token(username string) (string, error) {
|
||||
return m.Bearer, m.Error
|
||||
}
|
||||
func Test_Influx_AuthorizationBearer(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "Bearer ********"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{}`))
|
||||
got := r.Header.Get("Authorization")
|
||||
if got != want {
|
||||
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
bearer := &MockBearer{
|
||||
Bearer: "********",
|
||||
}
|
||||
|
||||
u, _ := url.Parse(ts.URL)
|
||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
||||
series := &influx.Client{
|
||||
URL: u,
|
||||
Bearer: bearer,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err := series.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error but was", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_AuthorizationBearerCtx(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{}`))
|
||||
got := r.Header.Get("Authorization")
|
||||
if got == "" {
|
||||
t.Error("Test_Influx_AuthorizationBearerCtx got empty string")
|
||||
}
|
||||
incomingToken := strings.Split(got, " ")[1]
|
||||
|
||||
alg := func(token *gojwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte("hunter2"), nil
|
||||
}
|
||||
claims := &gojwt.MapClaims{}
|
||||
token, err := gojwt.ParseWithClaims(string(incomingToken), claims, alg)
|
||||
if err != nil {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx unexpected claims error %v", err)
|
||||
}
|
||||
if !token.Valid {
|
||||
t.Error("Test_Influx_AuthorizationBearerCtx unexpected valid claim")
|
||||
}
|
||||
if err := claims.Valid(); err != nil {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx not expires already %v", err)
|
||||
}
|
||||
user := (*claims)["username"].(string)
|
||||
if user != "AzureDiamond" {
|
||||
t.Errorf("Test_Influx_AuthorizationBearerCtx expected username AzureDiamond but got %s", user)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
series := &influx.Client{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
err := series.Connect(context.Background(), &chronograf.Source{
|
||||
Username: "AzureDiamond",
|
||||
SharedSecret: "hunter2",
|
||||
URL: ts.URL,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err = series.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error but was", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
bearer := &MockBearer{
|
||||
Error: fmt.Errorf("cracked1337"),
|
||||
}
|
||||
|
||||
u, _ := url.Parse("http://haxored.net")
|
||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
||||
series := &influx.Client{
|
||||
URL: u,
|
||||
Bearer: bearer,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
query := chronograf.Query{
|
||||
Command: "show databases",
|
||||
}
|
||||
_, err := series.Query(context.Background(), query)
|
||||
if err == nil {
|
||||
t.Fatal("Test_Influx_AuthorizationBearerFailure Expected error but received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Influx_HTTPS_Failure(t *testing.T) {
|
||||
called := false
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
|
@ -53,7 +191,7 @@ func Test_Influx_HTTPS_Failure(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -97,7 +235,7 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
var series chronograf.TimeSeries
|
||||
series, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error initializing client: err:", err)
|
||||
}
|
||||
|
@ -166,7 +304,7 @@ func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
|||
ts.Close()
|
||||
}()
|
||||
|
||||
series, _ := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
series, _ := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
errs := make(chan (error))
|
||||
|
@ -209,7 +347,7 @@ func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_Influx_RejectsInvalidHosts(t *testing.T) {
|
||||
_, err := influx.NewClient(":", log.New(log.DebugLevel))
|
||||
_, err := NewClient(":", log.New(log.DebugLevel))
|
||||
if err == nil {
|
||||
t.Fatal("Expected err but was nil")
|
||||
}
|
||||
|
@ -221,7 +359,7 @@ func Test_Influx_ReportsInfluxErrs(t *testing.T) {
|
|||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cl, err := influx.NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
cl, err := NewClient(ts.URL, log.New(log.DebugLevel))
|
||||
if err != nil {
|
||||
t.Fatal("Encountered unexpected error while initializing influx client: err:", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// Bearer generates tokens for Authorization: Bearer
|
||||
type Bearer interface {
|
||||
Token(username string) (string, error)
|
||||
}
|
||||
|
||||
// BearerJWT is the default Bearer for InfluxDB
|
||||
type BearerJWT struct {
|
||||
SharedSecret string
|
||||
}
|
||||
|
||||
// Token returns the expected InfluxDB JWT signed with the sharedSecret
|
||||
func (b *BearerJWT) Token(username string) (string, error) {
|
||||
return JWT(username, b.SharedSecret, time.Now)
|
||||
}
|
||||
|
||||
// Now returns the current time
|
||||
type Now func() time.Time
|
||||
|
||||
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
|
||||
func JWT(username, sharedSecret string, now Now) (string, error) {
|
||||
token := &jwt.Token{
|
||||
Header: map[string]interface{}{
|
||||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS512.Alg(),
|
||||
},
|
||||
Claims: jwt.MapClaims{
|
||||
"username": username,
|
||||
"exp": now().Add(time.Minute).Unix(),
|
||||
},
|
||||
Method: jwt.SigningMethodHS512,
|
||||
}
|
||||
return token.SignedString([]byte(sharedSecret))
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJWT(t *testing.T) {
|
||||
type args struct {
|
||||
username string
|
||||
sharedSecret string
|
||||
now Now
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "",
|
||||
args: args{
|
||||
username: "AzureDiamond",
|
||||
sharedSecret: "hunter2",
|
||||
now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
},
|
||||
want: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjYwLCJ1c2VybmFtZSI6IkF6dXJlRGlhbW9uZCJ9.kUWGwcpCPwV7MEk7luO1rt8036LyvG4bRL_CfseQGmz4b0S34gATx30g4xvqVAV6bwwYE0YU3P8FjG8ij4kc5g",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := JWT(tt.args.username, tt.args.sharedSecret, tt.args.now)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWT() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("JWT() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package server
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
@ -51,7 +50,7 @@ type Server struct {
|
|||
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
|
||||
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
|
||||
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDb source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES"`
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"hunter2\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
|
||||
|
||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
|
@ -301,8 +300,13 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
|
||||
|
||||
go processNewSources(ctx, service, s.NewSources, logger)
|
||||
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("new-sources", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
basepath = s.Basepath
|
||||
if basepath != "" && s.PrefixRoutes == false {
|
||||
|
@ -437,33 +441,6 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
}
|
||||
}
|
||||
|
||||
// processNewSources parses and persists new sources passed in via server flag
|
||||
func processNewSources(ctx context.Context, service Service, newSources string, logger chronograf.Logger) error {
|
||||
if newSources == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var srcsKaps []chronograf.SourceAndKapacitor
|
||||
// On JSON unmarshal error, continue server process without new source and write error to log
|
||||
if err := json.Unmarshal([]byte(newSources), &srcsKaps); err != nil {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
}
|
||||
|
||||
// Add any new sources and kapacitors as specified via server flag
|
||||
if err := chronograf.NewSources(ctx, service.SourcesStore, service.ServersStore, srcsKaps, logger); err != nil {
|
||||
// Continue with server run even if adding NewSources fails
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportUsageStats starts periodic server reporting.
|
||||
func reportUsageStats(bi BuildInfo, logger chronograf.Logger) {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
|
|
@ -43,18 +43,15 @@ type InfluxClient struct{}
|
|||
|
||||
// New creates a client to connect to OSS or enterprise
|
||||
func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chronograf.TimeSeries, error) {
|
||||
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
|
||||
dataNode := &influx.Client{
|
||||
Logger: logger,
|
||||
}
|
||||
if err := dataNode.Connect(context.TODO(), &src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tls := strings.Contains(src.MetaURL, "https")
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, src.Username, src.Password, tls, dataNode)
|
||||
}
|
||||
return &influx.Client{
|
||||
client := &influx.Client{
|
||||
Logger: logger,
|
||||
}, nil
|
||||
}
|
||||
if err := client.Connect(context.TODO(), &src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
|
||||
tls := strings.Contains(src.MetaURL, "https")
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, src.Username, src.Password, tls, client)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
@ -34,8 +34,9 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
|
|||
src.Telegraf = "telegraf"
|
||||
}
|
||||
|
||||
// Omit the password on response
|
||||
// Omit the password and shared secret on response
|
||||
src.Password = ""
|
||||
src.SharedSecret = ""
|
||||
|
||||
httpAPISrcs := "/chronograf/v1/sources"
|
||||
res := sourceResponse{
|
||||
|
@ -99,6 +100,7 @@ func (h *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string,
|
|||
cli := &influx.Client{
|
||||
Logger: h.Logger,
|
||||
}
|
||||
|
||||
if err := cli.Connect(ctx, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -293,3 +295,69 @@ func ValidSourceRequest(s chronograf.Source) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleNewSources parses and persists new sources passed in via server flag
|
||||
func (h *Service) HandleNewSources(ctx context.Context, input string) error {
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var srcsKaps []struct {
|
||||
Source chronograf.Source `json:"influxdb"`
|
||||
Kapacitor chronograf.Server `json:"kapacitor"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(input), &srcsKaps); err != nil {
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSources", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sk := range srcsKaps {
|
||||
if err := ValidSourceRequest(sk.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add any new sources and kapacitors as specified via server flag
|
||||
if err := h.newSourceKapacitor(ctx, sk.Source, sk.Kapacitor); err != nil {
|
||||
// Continue with server run even if adding NewSource fails
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSource", "invalid").
|
||||
Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSourceKapacitor adds sources to BoltDB idempotently by name, as well as respective kapacitors
|
||||
func (h *Service) newSourceKapacitor(ctx context.Context, src chronograf.Source, kapa chronograf.Server) error {
|
||||
srcs, err := h.SourcesStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range srcs {
|
||||
// If source already exists, do nothing
|
||||
if s.Name == src.Name {
|
||||
h.Logger.
|
||||
WithField("component", "server").
|
||||
WithField("NewSource", s.Name).
|
||||
Info("Source already exists")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
src, err = h.SourcesStore.Add(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kapa.SrcID = src.ID
|
||||
if _, err := h.ServersStore.Add(ctx, kapa); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func Test_newSourceResponse(t *testing.T) {
|
||||
|
@ -66,3 +69,180 @@ func Test_newSourceResponse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_newSourceKapacitor(t *testing.T) {
|
||||
type fields struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
ServersStore chronograf.ServersStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
src chronograf.Source
|
||||
kapa chronograf.Server
|
||||
}
|
||||
srcCount := 0
|
||||
srvCount := 0
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantSrc int
|
||||
wantSrv int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Add when no existing sources",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return srv, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 1,
|
||||
wantSrv: 1,
|
||||
},
|
||||
{
|
||||
name: "Should not add if existing source",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{
|
||||
{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return srv, nil
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 0,
|
||||
wantSrv: 0,
|
||||
},
|
||||
{
|
||||
name: "Error if All returns error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return nil, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error if Add returns error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
return chronograf.Source{}, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error if kapa add is error",
|
||||
fields: fields{
|
||||
SourcesStore: &mocks.SourcesStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
|
||||
return []chronograf.Source{}, nil
|
||||
},
|
||||
AddF: func(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
srcCount++
|
||||
src.ID = srcCount
|
||||
return src, nil
|
||||
},
|
||||
},
|
||||
ServersStore: &mocks.ServersStore{
|
||||
AddF: func(ctx context.Context, srv chronograf.Server) (chronograf.Server, error) {
|
||||
srvCount++
|
||||
return chronograf.Server{}, fmt.Errorf("error")
|
||||
},
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
src: chronograf.Source{
|
||||
Name: "Influx 1",
|
||||
},
|
||||
kapa: chronograf.Server{
|
||||
Name: "Kapa 1",
|
||||
},
|
||||
},
|
||||
wantSrc: 1,
|
||||
wantSrv: 1,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srcCount = 0
|
||||
srvCount = 0
|
||||
h := &Service{
|
||||
SourcesStore: tt.fields.SourcesStore,
|
||||
ServersStore: tt.fields.ServersStore,
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
if err := h.newSourceKapacitor(tt.args.ctx, tt.args.src, tt.args.kapa); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Service.newSourceKapacitor() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantSrc != srcCount {
|
||||
t.Errorf("Service.newSourceKapacitor() count = %d, wantSrc %d", srcCount, tt.wantSrc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
8085
server/swagger.json
8085
server/swagger.json
File diff suppressed because it is too large
Load Diff
|
@ -17,7 +17,8 @@
|
|||
"test:lint": "npm run lint; npm run test",
|
||||
"test:dev": "nodemon --exec npm run test:lint",
|
||||
"clean": "rm -rf build",
|
||||
"storybook": "node ./storybook"
|
||||
"storybook": "node ./storybook",
|
||||
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
|
||||
},
|
||||
"author": "",
|
||||
"eslintConfig": {
|
||||
|
@ -76,6 +77,7 @@
|
|||
"postcss-loader": "^0.8.0",
|
||||
"postcss-reporter": "^1.3.1",
|
||||
"precss": "^1.4.0",
|
||||
"prettier": "^1.5.3",
|
||||
"react-addons-test-utils": "^15.0.2",
|
||||
"resolve-url-loader": "^1.6.0",
|
||||
"sass-loader": "^3.2.0",
|
||||
|
|
|
@ -88,11 +88,17 @@ const AdminTabs = ({
|
|||
return (
|
||||
<Tabs className="row">
|
||||
<TabList customClass="col-md-2 admin-tabs">
|
||||
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
|
||||
{tabs.map((t, i) =>
|
||||
<Tab key={tabs[i].type}>
|
||||
{tabs[i].type}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanels customClass="col-md-10 admin-tabs--content">
|
||||
{tabs.map((t, i) =>
|
||||
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
|
||||
<TabPanel key={tabs[i].type}>
|
||||
{t.component}
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
|
|
@ -52,9 +52,7 @@ const DatabaseTable = ({
|
|||
<table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Retention Policy
|
||||
</th>
|
||||
<th>Retention Policy</th>
|
||||
<th style={{width: `${DATABASE_TABLE.colDuration}px`}}>
|
||||
Duration
|
||||
</th>
|
||||
|
|
|
@ -105,7 +105,9 @@ const Header = ({
|
|||
|
||||
return (
|
||||
<div className="db-manager-header">
|
||||
<h4>{database.name}</h4>
|
||||
<h4>
|
||||
{database.name}
|
||||
</h4>
|
||||
{database.hasOwnProperty('deleteCode') ? deleteConfirmation : buttons}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,9 @@ import React, {PropTypes} from 'react'
|
|||
const EmptyRow = ({tableName}) =>
|
||||
<tr className="table-empty-state">
|
||||
<th colSpan="5">
|
||||
<p>You don't have any {tableName},<br />why not create one?</p>
|
||||
<p>
|
||||
You don't have any {tableName},<br />why not create one?
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ const QueriesTable = ({queries, onKillQuery}) =>
|
|||
Database
|
||||
</th>
|
||||
<th>Query</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colRunning}px`}}>
|
||||
Running
|
||||
</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colRunning}px`}}>Running</th>
|
||||
<th style={{width: `${QUERIES_TABLE.colKillQuery}px`}} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
@ -39,7 +39,11 @@ class QueryRow extends Component {
|
|||
>
|
||||
{database}
|
||||
</td>
|
||||
<td><code>{query}</code></td>
|
||||
<td>
|
||||
<code>
|
||||
{query}
|
||||
</code>
|
||||
</td>
|
||||
<td
|
||||
style={{width: `${QUERIES_TABLE.colRunning}px`}}
|
||||
className="monotype"
|
||||
|
|
|
@ -61,7 +61,9 @@ const RoleRow = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: `${ROLES_TABLE.colName}px`}}>{name}</td>
|
||||
<td style={{width: `${ROLES_TABLE.colName}px`}}>
|
||||
{name}
|
||||
</td>
|
||||
<td>
|
||||
{allPermissions && allPermissions.length
|
||||
? <MultiSelectDropdown
|
||||
|
|
|
@ -71,7 +71,9 @@ const UserRow = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td style={{width: `${USERS_TABLE.colUsername}px`}}>{name}</td>
|
||||
<td style={{width: `${USERS_TABLE.colUsername}px`}}>
|
||||
{name}
|
||||
</td>
|
||||
<td style={{width: `${USERS_TABLE.colPassword}px`}}>
|
||||
<ChangePassRow
|
||||
onEdit={onEdit}
|
||||
|
|
|
@ -166,9 +166,7 @@ class AdminPage extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Admin
|
||||
</h1>
|
||||
<h1 className="page-header__title">Admin</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
|
|
|
@ -48,9 +48,8 @@ class AlertsTable extends Component {
|
|||
changeSort(key) {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
if (this.state.sortKey === key) {
|
||||
const reverseDirection = this.state.sortDirection === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
const reverseDirection =
|
||||
this.state.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
this.setState({sortDirection: reverseDirection})
|
||||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
|
@ -192,9 +191,7 @@ class AlertsTable extends Component {
|
|||
</p>
|
||||
</div>
|
||||
: <div className="generic-empty-state">
|
||||
<h4 className="no-user-select">
|
||||
There are no Alerts to display
|
||||
</h4>
|
||||
<h4 className="no-user-select">There are no Alerts to display</h4>
|
||||
<br />
|
||||
<h6 className="no-user-select">
|
||||
Try changing the Time Range or
|
||||
|
@ -236,7 +233,9 @@ class AlertsTable extends Component {
|
|||
</div>
|
||||
: <div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{this.props.alerts.length} Alerts</h2>
|
||||
<h2 className="panel-title">
|
||||
{this.props.alerts.length} Alerts
|
||||
</h2>
|
||||
{this.props.alerts.length
|
||||
? <SearchBar onSearch={this.filterAlerts} />
|
||||
: null}
|
||||
|
|
|
@ -153,9 +153,7 @@ class AlertsApp extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Alert History
|
||||
</h1>
|
||||
<h1 className="page-header__title">Alert History</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
|
|
|
@ -15,7 +15,9 @@ const Login = ({authData: {auth}}) => {
|
|||
<div className="auth-box">
|
||||
<div className="auth-logo" />
|
||||
<h1 className="auth-text-logo">Chronograf</h1>
|
||||
<p><strong>{VERSION}</strong> / Time-Series Data Visualization</p>
|
||||
<p>
|
||||
<strong>{VERSION}</strong> / Time-Series Data Visualization
|
||||
</p>
|
||||
{auth.links &&
|
||||
auth.links.map(({name, login, label}) =>
|
||||
<a key={name} className="btn btn-primary" href={login}>
|
||||
|
|
|
@ -14,9 +14,7 @@ import {errorThrown} from 'shared/actions/errors'
|
|||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||
|
||||
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||
import {
|
||||
makeQueryForTemplate,
|
||||
} from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||
import {makeQueryForTemplate} from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||
import parsers from 'shared/parsing'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
|
|
|
@ -26,19 +26,16 @@ const Dashboard = ({
|
|||
}) => {
|
||||
const cells = dashboard.cells.map(cell => {
|
||||
const dashboardCell = {...cell}
|
||||
dashboardCell.queries = dashboardCell.queries.map(({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
}) => ({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
database: db,
|
||||
text: query,
|
||||
}))
|
||||
dashboardCell.queries = dashboardCell.queries.map(
|
||||
({label, query, queryConfig, db}) => ({
|
||||
label,
|
||||
query,
|
||||
queryConfig,
|
||||
db,
|
||||
database: db,
|
||||
text: query,
|
||||
})
|
||||
)
|
||||
return dashboardCell
|
||||
})
|
||||
|
||||
|
|
|
@ -35,7 +35,9 @@ const DashboardHeader = ({
|
|||
type="button"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<span>{buttonText}</span>
|
||||
<span>
|
||||
{buttonText}
|
||||
</span>
|
||||
<span className="caret" />
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
|
|
|
@ -7,9 +7,7 @@ const DashboardsHeader = ({sourceName}) => {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Dashboards
|
||||
</h1>
|
||||
<h1 className="page-header__title">Dashboards</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={sourceName} />
|
||||
|
|
|
@ -25,7 +25,9 @@ const DashboardsPageContents = ({
|
|||
<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">{tableHeader}</h2>
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
|
|
|
@ -34,9 +34,7 @@ const DashboardsTable = ({
|
|||
{tv.tempVar}
|
||||
</code>
|
||||
)
|
||||
: <span className="empty-string">
|
||||
None
|
||||
</span>}
|
||||
: <span className="empty-string">None</span>}
|
||||
</td>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
|
|
|
@ -90,7 +90,11 @@ const TemplateQueryBuilder = ({
|
|||
</div>
|
||||
)
|
||||
default:
|
||||
return <div><span className="tvm-query-builder--text">n/a</span></div>
|
||||
return (
|
||||
<div>
|
||||
<span className="tvm-query-builder--text">n/a</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -198,7 +198,9 @@ const TableInput = ({
|
|||
/>
|
||||
</div>
|
||||
: <div style={{width: '100%'}} onClick={() => onStartEdit(name)}>
|
||||
<div className="tvm-input">{defaultValue}</div>
|
||||
<div className="tvm-input">
|
||||
{defaultValue}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -217,12 +217,12 @@ export default function ui(state = initialState, action) {
|
|||
|
||||
const dashboards = state.dashboards.map(
|
||||
dashboard =>
|
||||
(dashboard.id === dashboardID
|
||||
dashboard.id === dashboardID
|
||||
? {
|
||||
...dashboard,
|
||||
templates: dashboard.templates.map(
|
||||
template =>
|
||||
(template.id === templateID
|
||||
template.id === templateID
|
||||
? {
|
||||
...template,
|
||||
values: values.map((value, i) => ({
|
||||
|
@ -231,10 +231,10 @@ export default function ui(state = initialState, action) {
|
|||
type: TEMPLATE_VARIABLE_TYPES[template.type],
|
||||
})),
|
||||
}
|
||||
: template)
|
||||
: template
|
||||
),
|
||||
}
|
||||
: dashboard)
|
||||
: dashboard
|
||||
)
|
||||
|
||||
return {...state, dashboards}
|
||||
|
|
|
@ -39,7 +39,11 @@ export function toggleField(queryId, fieldFunc, isKapacitorRule) {
|
|||
|
||||
// all fields implicitly have a function applied to them, so consequently
|
||||
// we need to set the auto group by time
|
||||
export const toggleFieldWithGroupByInterval = (queryID, fieldFunc, isKapacitorRule) => (dispatch) => {
|
||||
export const toggleFieldWithGroupByInterval = (
|
||||
queryID,
|
||||
fieldFunc,
|
||||
isKapacitorRule
|
||||
) => dispatch => {
|
||||
dispatch(toggleField(queryID, fieldFunc, isKapacitorRule))
|
||||
dispatch(groupByTime(queryID, DEFAULT_DATA_EXPLORER_GROUP_BY_INTERVAL))
|
||||
}
|
||||
|
|
|
@ -7,12 +7,7 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
import {showFieldKeys} from 'shared/apis/metaQuery'
|
||||
import showFieldKeysParser from 'shared/parsing/showFieldKeys'
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
const FieldList = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -112,7 +107,9 @@ const FieldList = React.createClass({
|
|||
if (!database || !measurement) {
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Measurement</strong> selected</span>
|
||||
<span>
|
||||
No <strong>Measurement</strong> selected
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,18 @@ const GroupByTimeDropdown = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {selected, onChooseGroupByTime, isInRuleBuilder, isInDataExplorer} = this.props
|
||||
const {
|
||||
selected,
|
||||
onChooseGroupByTime,
|
||||
isInRuleBuilder,
|
||||
isInDataExplorer,
|
||||
} = this.props
|
||||
|
||||
let validOptions = groupByTimeOptions
|
||||
if (isInDataExplorer) {
|
||||
validOptions = validOptions.filter(({menuOption}) => menuOption !== DEFAULT_DASHBOARD_GROUP_BY_INTERVAL)
|
||||
validOptions = validOptions.filter(
|
||||
({menuOption}) => menuOption !== DEFAULT_DASHBOARD_GROUP_BY_INTERVAL
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -112,7 +112,9 @@ const MeasurementList = React.createClass({
|
|||
if (!this.props.query.database) {
|
||||
return (
|
||||
<div className="query-builder--list-empty">
|
||||
<span>No <strong>Database</strong> selected</span>
|
||||
<span>
|
||||
No <strong>Database</strong> selected
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,12 @@ const NoDataNodeError = React.createClass({
|
|||
render() {
|
||||
return (
|
||||
<ClusterError>
|
||||
<PanelHeading>{errorCopy.noData.head}</PanelHeading>
|
||||
<PanelBody>{errorCopy.noData.body}</PanelBody>
|
||||
<PanelHeading>
|
||||
{errorCopy.noData.head}
|
||||
</PanelHeading>
|
||||
<PanelBody>
|
||||
{errorCopy.noData.body}
|
||||
</PanelBody>
|
||||
</ClusterError>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -52,7 +52,10 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
this.props.actions.toggleFieldWithGroupByInterval(this.props.query.id, field)
|
||||
this.props.actions.toggleFieldWithGroupByInterval(
|
||||
this.props.query.id,
|
||||
field
|
||||
)
|
||||
},
|
||||
|
||||
handleGroupByTime(time) {
|
||||
|
@ -60,7 +63,11 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
handleApplyFuncsToField(fieldFunc) {
|
||||
this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc, this.props.isInDataExplorer)
|
||||
this.props.actions.applyFuncsToField(
|
||||
this.props.query.id,
|
||||
fieldFunc,
|
||||
this.props.isInDataExplorer
|
||||
)
|
||||
},
|
||||
|
||||
handleChooseTag(tag) {
|
||||
|
|
|
@ -225,7 +225,9 @@ class QueryEditor extends Component {
|
|||
className={classnames('varmoji', {'varmoji-rotated': isTemplating})}
|
||||
>
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">{this.renderStatus(status)}</div>
|
||||
<div className="varmoji-front">
|
||||
{this.renderStatus(status)}
|
||||
</div>
|
||||
<div className="varmoji-back">
|
||||
{isTemplating
|
||||
? <TemplateDrawer
|
||||
|
|
|
@ -30,7 +30,9 @@ const QueryMakerTab = React.createClass({
|
|||
})}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
<label>{this.props.queryTabText}</label>
|
||||
<label>
|
||||
{this.props.queryTabText}
|
||||
</label>
|
||||
<span className="query-maker--delete" onClick={this.handleDelete} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -27,10 +27,18 @@ const CustomCell = React.createClass({
|
|||
if (columnName === 'time') {
|
||||
const date = moment(new Date(data)).format('MM/DD/YY hh:mm:ssA')
|
||||
|
||||
return <span>{date}</span>
|
||||
return (
|
||||
<span>
|
||||
{date}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <span>{data}</span>
|
||||
return (
|
||||
<span>
|
||||
{data}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -137,9 +145,8 @@ const ChronoTable = React.createClass({
|
|||
const headerHeight = 30
|
||||
const minWidth = 70
|
||||
const styleAdjustedHeight = height - stylePixelOffset
|
||||
const width = columns && columns.length > 1
|
||||
? defaultColumnWidth
|
||||
: containerWidth
|
||||
const width =
|
||||
columns && columns.length > 1 ? defaultColumnWidth : containerWidth
|
||||
|
||||
if (!query) {
|
||||
return <div className="generic-empty-state">Please add a query below</div>
|
||||
|
@ -172,9 +179,7 @@ const ChronoTable = React.createClass({
|
|||
/>}
|
||||
<div className="table--tabs-content">
|
||||
{(columns && !columns.length) || (values && !values.length)
|
||||
? <div className="generic-empty-state">
|
||||
This series is empty
|
||||
</div>
|
||||
? <div className="generic-empty-state">This series is empty</div>
|
||||
: <Table
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
isColumnResizing={false}
|
||||
|
@ -191,7 +196,11 @@ const ChronoTable = React.createClass({
|
|||
isResizable={true}
|
||||
key={columnName}
|
||||
columnKey={columnName}
|
||||
header={<Cell>{columnName}</Cell>}
|
||||
header={
|
||||
<Cell>
|
||||
{columnName}
|
||||
</Cell>
|
||||
}
|
||||
cell={({rowIndex}) =>
|
||||
<CustomCell
|
||||
columnName={columnName}
|
||||
|
|
|
@ -17,7 +17,9 @@ const VisHeader = ({views, view, onToggleView, name}) =>
|
|||
)}
|
||||
</ul>
|
||||
: null}
|
||||
<div className="graph-title">{name}</div>
|
||||
<div className="graph-title">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {arrayOf, func, string} = PropTypes
|
||||
|
|
|
@ -12,7 +12,7 @@ const WriteDataBody = ({
|
|||
isManual,
|
||||
fileInput,
|
||||
handleFileOpen,
|
||||
}) => (
|
||||
}) =>
|
||||
<div className="write-data-form--body">
|
||||
{isManual
|
||||
? <textarea
|
||||
|
@ -51,7 +51,6 @@ const WriteDataBody = ({
|
|||
uploadContent={uploadContent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const WriteDataFooter = ({
|
|||
inputContent,
|
||||
uploadContent,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
}) =>
|
||||
<div className="write-data-form--footer">
|
||||
{isManual
|
||||
? <span className="write-data-form--helper">
|
||||
|
@ -33,7 +33,6 @@ const WriteDataFooter = ({
|
|||
Write
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ class WriteDataForm extends Component {
|
|||
/>
|
||||
<WriteDataBody
|
||||
{...this.state}
|
||||
fileInput={el => this.fileInput = el}
|
||||
fileInput={el => (this.fileInput = el)}
|
||||
handleEdit={this.handleEdit}
|
||||
handleFile={this.handleFile}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
|
|
|
@ -8,7 +8,7 @@ const WriteDataHeader = ({
|
|||
toggleWriteView,
|
||||
isManual,
|
||||
onClose,
|
||||
}) => (
|
||||
}) =>
|
||||
<div className="write-data-form--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Write Data To</h1>
|
||||
|
@ -36,7 +36,6 @@ const WriteDataHeader = ({
|
|||
<span className="page-header__dismiss" onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {func, string, bool} = PropTypes
|
||||
|
||||
|
|
|
@ -44,9 +44,7 @@ const Header = React.createClass({
|
|||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Data Explorer
|
||||
</h1>
|
||||
<h1 className="page-header__title">Data Explorer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
|
|
|
@ -130,7 +130,11 @@ export default function queryConfigs(state = {}, action) {
|
|||
|
||||
case 'APPLY_FUNCS_TO_FIELD': {
|
||||
const {queryId, fieldFunc, isInDataExplorer} = action.payload
|
||||
const nextQueryConfig = applyFuncsToField(state[queryId], fieldFunc, isInDataExplorer)
|
||||
const nextQueryConfig = applyFuncsToField(
|
||||
state[queryId],
|
||||
fieldFunc,
|
||||
isInDataExplorer
|
||||
)
|
||||
|
||||
return Object.assign({}, state, {
|
||||
[queryId]: nextQueryConfig,
|
||||
|
|
|
@ -73,9 +73,8 @@ const HostsTable = React.createClass({
|
|||
updateSort(key) {
|
||||
// if we're using the key, reverse order; otherwise, set it with ascending
|
||||
if (this.state.sortKey === key) {
|
||||
const reverseDirection = this.state.sortDirection === 'asc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
const reverseDirection =
|
||||
this.state.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
this.setState({sortDirection: reverseDirection})
|
||||
} else {
|
||||
this.setState({sortKey: key, sortDirection: 'asc'})
|
||||
|
@ -118,7 +117,9 @@ const HostsTable = React.createClass({
|
|||
return (
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{hostsTitle}</h2>
|
||||
<h2 className="panel-title">
|
||||
{hostsTitle}
|
||||
</h2>
|
||||
<SearchBar onSearch={this.updateSearchTerm} />
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
|
@ -165,9 +166,7 @@ const HostsTable = React.createClass({
|
|||
</tbody>
|
||||
</table>
|
||||
: <div className="generic-empty-state">
|
||||
<h4 style={{margin: '90px 0'}}>
|
||||
No Hosts found
|
||||
</h4>
|
||||
<h4 style={{margin: '90px 0'}}>No Hosts found</h4>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -202,7 +201,9 @@ const HostRow = React.createClass({
|
|||
return (
|
||||
<tr>
|
||||
<td style={{width: colName}}>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</td>
|
||||
<td style={{width: colStatus}}>
|
||||
<div
|
||||
|
|
|
@ -81,9 +81,7 @@ export const HostsPage = React.createClass({
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Host List
|
||||
</h1>
|
||||
<h1 className="page-header__title">Host List</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator sourceName={source.name} />
|
||||
|
|
|
@ -13,9 +13,7 @@ class KapacitorForm extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
Configure Kapacitor
|
||||
</h1>
|
||||
<h1 className="page-header__title">Configure Kapacitor</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,9 +44,8 @@ const KapacitorRules = ({
|
|||
)
|
||||
}
|
||||
|
||||
const tableHeader = rules.length === 1
|
||||
? '1 Alert Rule'
|
||||
: `${rules.length} Alert Rules`
|
||||
const tableHeader =
|
||||
rules.length === 1 ? '1 Alert Rule' : `${rules.length} Alert Rules`
|
||||
return (
|
||||
<PageContents
|
||||
source={source}
|
||||
|
@ -55,7 +54,9 @@ const KapacitorRules = ({
|
|||
onCloseTickscript={onCloseTickscript}
|
||||
>
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">{tableHeader}</h2>
|
||||
<h2 className="panel-title">
|
||||
{tableHeader}
|
||||
</h2>
|
||||
<Link
|
||||
to={`/sources/${source.id}/alert-rules/new`}
|
||||
className="btn btn-sm btn-primary"
|
||||
|
@ -74,7 +75,7 @@ const KapacitorRules = ({
|
|||
)
|
||||
}
|
||||
|
||||
const PageContents = ({children, source, tickscript, onCloseTickscript}) => (
|
||||
const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
|
@ -104,7 +105,6 @@ const PageContents = ({children, source, tickscript, onCloseTickscript}) => (
|
|||
/>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {arrayOf, bool, func, node, shape, string} = PropTypes
|
||||
|
||||
|
|
|
@ -27,7 +27,9 @@ const KapacitorRulesTable = ({
|
|||
<th style={{width: colType}}>Rule Type</th>
|
||||
<th style={{width: colMessage}}>Message</th>
|
||||
<th style={{width: colAlerts}}>Alerts</th>
|
||||
<th style={{width: colEnabled}} className="text-center">Enabled</th>
|
||||
<th style={{width: colEnabled}} className="text-center">
|
||||
Enabled
|
||||
</th>
|
||||
<th style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -53,7 +55,9 @@ const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
|
|||
<td style={{width: colName}} className="monotype">
|
||||
<RuleTitle rule={rule} source={source} />
|
||||
</td>
|
||||
<td style={{width: colType}} className="monotype">{rule.trigger}</td>
|
||||
<td style={{width: colType}} className="monotype">
|
||||
{rule.trigger}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<span
|
||||
className="table-cell-nowrap"
|
||||
|
@ -90,10 +94,18 @@ const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
|
|||
const RuleTitle = ({rule: {id, name, query}, source}) => {
|
||||
// no queryConfig means the rule was manually created outside of Chronograf
|
||||
if (!query) {
|
||||
return <i>{name}</i>
|
||||
return (
|
||||
<i>
|
||||
{name}
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
return <Link to={`/sources/${source.id}/alert-rules/${id}`}>{name}</Link>
|
||||
return (
|
||||
<Link to={`/sources/${source.id}/alert-rules/${id}`}>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
|
|
@ -34,7 +34,9 @@ export const RuleGraph = React.createClass({
|
|||
if (!queryText) {
|
||||
return (
|
||||
<div className="rule-builder--graph-empty">
|
||||
<p>Select a <strong>Time-Series</strong> to preview on a graph</p>
|
||||
<p>
|
||||
Select a <strong>Time-Series</strong> to preview on a graph
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -106,9 +108,10 @@ export const RuleGraph = React.createClass({
|
|||
const bottom = dygraph.toDomYCoord(highlightStart)
|
||||
const top = dygraph.toDomYCoord(highlightEnd)
|
||||
|
||||
canvas.fillStyle = rule.values.operator === 'outside range'
|
||||
? 'rgba(41, 41, 51, 1)'
|
||||
: 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillStyle =
|
||||
rule.values.operator === 'outside range'
|
||||
? 'rgba(41, 41, 51, 1)'
|
||||
: 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillRect(area.x, top, area.w, bottom - top)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,7 +28,9 @@ export const ValuesSection = React.createClass({
|
|||
<Tabs initialIndex={initialIndex} onSelect={this.handleChooseTrigger}>
|
||||
<TabList isKapacitorTabs="true">
|
||||
{TABS.map(tab =>
|
||||
<Tab key={tab} isKapacitorTab={true}>{tab}</Tab>
|
||||
<Tab key={tab} isKapacitorTab={true}>
|
||||
{tab}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
|
||||
|
|
|
@ -76,11 +76,8 @@ const OpsGenieConfig = React.createClass({
|
|||
refFunc={r => (this.apiKey = r)}
|
||||
/>
|
||||
<label className="form-helper">
|
||||
Note: a value of
|
||||
{' '}
|
||||
<code>true</code>
|
||||
{' '}
|
||||
indicates the OpsGenie API key has been set
|
||||
Note: a value of <code>true</code> indicates the OpsGenie API key
|
||||
has been set
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -138,7 +135,9 @@ const TagInput = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor={title}>{title}</label>
|
||||
<label htmlFor={title}>
|
||||
{title}
|
||||
</label>
|
||||
<input
|
||||
placeholder={`Type and hit 'Enter' to add to list of ${title}`}
|
||||
autoComplete="off"
|
||||
|
@ -183,7 +182,9 @@ const Tag = React.createClass({
|
|||
|
||||
return (
|
||||
<span key={item} className="input-tag-item">
|
||||
<span>{item}</span>
|
||||
<span>
|
||||
{item}
|
||||
</span>
|
||||
<span className="icon remove" onClick={() => onDelete(item)} />
|
||||
</span>
|
||||
)
|
||||
|
|
|
@ -39,11 +39,8 @@ const PagerDutyConfig = React.createClass({
|
|||
defaultValue={serviceKey || ''}
|
||||
/>
|
||||
<label className="form-helper">
|
||||
Note: a value of
|
||||
{' '}
|
||||
<code>true</code>
|
||||
{' '}
|
||||
indicates the PagerDuty service key has been set
|
||||
Note: a value of <code>true</code> indicates the PagerDuty service
|
||||
key has been set
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -55,15 +55,13 @@ const TelegramConfig = React.createClass({
|
|||
<div className="form-group col-xs-12">
|
||||
<div className="alert alert-warning alert-icon no-user-select">
|
||||
<span className="icon triangle" />
|
||||
You need a
|
||||
{' '}
|
||||
You need a{' '}
|
||||
<a
|
||||
href="https://docs.influxdata.com/kapacitor/latest/guides/event-handler-setup/#telegram-setup"
|
||||
target="_blank"
|
||||
>
|
||||
Telegram Bot
|
||||
</a>
|
||||
{' '}
|
||||
</a>{' '}
|
||||
to use this endpoint
|
||||
</div>
|
||||
</div>
|
||||
|
@ -137,12 +135,10 @@ const TelegramConfig = React.createClass({
|
|||
ref={r => (this.disableWebPagePreview = r)}
|
||||
/>
|
||||
<label htmlFor="disableWebPagePreview">
|
||||
Disable
|
||||
{' '}
|
||||
Disable{' '}
|
||||
<a href="https://telegram.org/blog/link-preview" target="_blank">
|
||||
link previews
|
||||
</a>
|
||||
{' '}
|
||||
</a>{' '}
|
||||
in alert messages.
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -19,11 +19,7 @@ export const KapacitorTasksPage = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="kapacitorTasks">
|
||||
tasks
|
||||
</div>
|
||||
)
|
||||
return <div className="kapacitorTasks">tasks</div>
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -168,9 +168,11 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
|
|||
}
|
||||
|
||||
export function testAlertOutput(kapacitor, outputName, properties) {
|
||||
return kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/service-tests').then(({
|
||||
data: {services},
|
||||
}) => {
|
||||
return kapacitorProxy(
|
||||
kapacitor,
|
||||
'GET',
|
||||
'/kapacitor/v1/service-tests'
|
||||
).then(({data: {services}}) => {
|
||||
const service = services.find(s => s.name === outputName)
|
||||
return kapacitorProxy(
|
||||
kapacitor,
|
||||
|
|
|
@ -55,7 +55,9 @@ const AutoRefreshDropdown = React.createClass({
|
|||
+milliseconds > 0 ? 'refresh' : 'pause'
|
||||
)}
|
||||
/>
|
||||
<span className="dropdown-selected">{inputValue}</span>
|
||||
<span className="dropdown-selected">
|
||||
{inputValue}
|
||||
</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
<ul className="dropdown-menu">
|
||||
|
|
|
@ -2,30 +2,24 @@ import React, {PropTypes} from 'react'
|
|||
import classnames from 'classnames'
|
||||
import OnClickOutside from 'react-onclickoutside'
|
||||
|
||||
const ContextMenu = OnClickOutside(({
|
||||
isOpen,
|
||||
toggleMenu,
|
||||
onEdit,
|
||||
onRename,
|
||||
onDelete,
|
||||
cell,
|
||||
}) => (
|
||||
<div
|
||||
className={classnames('dash-graph--options', {
|
||||
'dash-graph--options-show': isOpen,
|
||||
})}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<button className="btn btn-info btn-xs">
|
||||
<span className="icon caret-down" />
|
||||
</button>
|
||||
<ul className="dash-graph--options-menu">
|
||||
<li onClick={() => onEdit(cell)}>Edit</li>
|
||||
<li onClick={onRename(cell.x, cell.y, cell.isEditing)}>Rename</li>
|
||||
<li onClick={() => onDelete(cell)}>Delete</li>
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
const ContextMenu = OnClickOutside(
|
||||
({isOpen, toggleMenu, onEdit, onRename, onDelete, cell}) =>
|
||||
<div
|
||||
className={classnames('dash-graph--options', {
|
||||
'dash-graph--options-show': isOpen,
|
||||
})}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<button className="btn btn-info btn-xs">
|
||||
<span className="icon caret-down" />
|
||||
</button>
|
||||
<ul className="dash-graph--options-menu">
|
||||
<li onClick={() => onEdit(cell)}>Edit</li>
|
||||
<li onClick={onRename(cell.x, cell.y, cell.isEditing)}>Rename</li>
|
||||
<li onClick={() => onDelete(cell)}>Delete</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ContextMenuContainer = props => {
|
||||
if (!props.isEditable) {
|
||||
|
|
|
@ -14,7 +14,11 @@ const CustomTimeIndicator = ({queries}) => {
|
|||
? `${customLower} AND ${customUpper}`
|
||||
: customLower
|
||||
|
||||
return <span className="dash-graph--custom-time">{customTimeRange}</span>
|
||||
return (
|
||||
<span className="dash-graph--custom-time">
|
||||
{customTimeRange}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, shape} = PropTypes
|
||||
|
|
|
@ -150,7 +150,11 @@ class Dropdown extends Component {
|
|||
autoHeight={true}
|
||||
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
|
||||
>
|
||||
{menuLabel ? <li className="dropdown-header">{menuLabel}</li> : null}
|
||||
{menuLabel
|
||||
? <li className="dropdown-header">
|
||||
{menuLabel}
|
||||
</li>
|
||||
: null}
|
||||
{menuItems.map((item, i) => {
|
||||
if (item.text === 'SEPARATOR') {
|
||||
return <li key={i} className="dropdown-divider" />
|
||||
|
@ -246,7 +250,9 @@ class Dropdown extends Component {
|
|||
{iconName
|
||||
? <span className={classnames('icon', {[iconName]: true})} />
|
||||
: null}
|
||||
<span className="dropdown-selected">{selected}</span>
|
||||
<span className="dropdown-selected">
|
||||
{selected}
|
||||
</span>
|
||||
<span className="caret" />
|
||||
</div>}
|
||||
{isOpen && menuItems.length ? this.renderMenu() : null}
|
||||
|
|
|
@ -69,7 +69,9 @@ const DygraphLegend = ({
|
|||
onMouseLeave={onHide}
|
||||
>
|
||||
<div className="dygraph-legend--header">
|
||||
<div className="dygraph-legend--timestamp">{xHTML}</div>
|
||||
<div className="dygraph-legend--timestamp">
|
||||
{xHTML}
|
||||
</div>
|
||||
{renderSortAlpha}
|
||||
{renderSortNum}
|
||||
<button
|
||||
|
@ -106,7 +108,9 @@ const DygraphLegend = ({
|
|||
<span style={{color}}>
|
||||
{isSnipped ? removeMeasurement(label) : label}
|
||||
</span>
|
||||
<figure>{yHTML || 'no value'}</figure>
|
||||
<figure>
|
||||
{yHTML || 'no value'}
|
||||
</figure>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -183,7 +183,9 @@ export default React.createClass({
|
|||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})}
|
||||
>
|
||||
<span className="single-stat--shadow">{roundedValue}</span>
|
||||
<span className="single-stat--shadow">
|
||||
{roundedValue}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
: null}
|
||||
|
|
|
@ -74,8 +74,14 @@ export default React.createClass({
|
|||
|
||||
const statText = (
|
||||
<div className="cluster-stat--label">
|
||||
<span>{this.props.queryDescription}</span>
|
||||
<span><strong>{truncated}</strong></span>
|
||||
<span>
|
||||
{this.props.queryDescription}
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
{truncated}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
@ -118,7 +118,9 @@ class MultiSelectDropdown extends Component {
|
|||
onClick={_.wrap(listItem, this.onSelect)}
|
||||
>
|
||||
<div className="multi-select--checkbox" />
|
||||
<span>{listItem}</span>
|
||||
<span>
|
||||
{listItem}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -14,7 +14,8 @@ const NoKapacitorError = React.createClass({
|
|||
<div className="graph-empty">
|
||||
<p>
|
||||
The current source does not have an associated Kapacitor instance
|
||||
<br /><br />
|
||||
<br />
|
||||
<br />
|
||||
<Link to={path} className="btn btn-sm btn-primary">
|
||||
Configure Kapacitor
|
||||
</Link>
|
||||
|
|
|
@ -35,7 +35,8 @@ class Notifications extends Component {
|
|||
})
|
||||
return (
|
||||
<div className={cls} role="alert">
|
||||
{message}{this.renderDismiss(type)}
|
||||
{message}
|
||||
{this.renderDismiss(type)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ import React, {PropTypes} from 'react'
|
|||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||
|
||||
const OverlayTechnologies = ({children}) =>
|
||||
<div className={OVERLAY_TECHNOLOGY}>{children}</div>
|
||||
<div className={OVERLAY_TECHNOLOGY}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
const {node} = PropTypes
|
||||
|
||||
|
|
|
@ -70,12 +70,18 @@ export const TabList = React.createClass({
|
|||
if (this.props.customClass) {
|
||||
return (
|
||||
<div className={this.props.customClass}>
|
||||
<div className="btn-group btn-group-lg tab-group">{children}</div>
|
||||
<div className="btn-group btn-group-lg tab-group">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="btn-group btn-group-lg tab-group">{children}</div>
|
||||
return (
|
||||
<div className="btn-group btn-group-lg tab-group">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -102,7 +108,11 @@ export const TabPanel = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
return <div>{this.props.children}</div>
|
||||
return (
|
||||
<div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -151,6 +161,10 @@ export const Tabs = React.createClass({
|
|||
return child
|
||||
})
|
||||
|
||||
return <div className={this.props.tabContentsClass}>{children}</div>
|
||||
return (
|
||||
<div className={this.props.tabContentsClass}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,7 +3,9 @@ import ReactTooltip from 'react-tooltip'
|
|||
|
||||
const Tooltip = ({tip, children}) =>
|
||||
<div>
|
||||
<div data-tip={tip}>{children}</div>
|
||||
<div data-tip={tip}>
|
||||
{children}
|
||||
</div>
|
||||
<ReactTooltip
|
||||
effect="solid"
|
||||
html={true}
|
||||
|
|
|
@ -65,9 +65,10 @@ export const multiColumnBarPlotter = e => {
|
|||
for (let i = 0; i < sets[j].length; i++) {
|
||||
const p = sets[j][i]
|
||||
const centerX = p.canvasx
|
||||
const xLeft = sets.length === 1
|
||||
? centerX - barWidth / 2
|
||||
: centerX - barWidth / 2 * (1 - j / (sets.length - 1))
|
||||
const xLeft =
|
||||
sets.length === 1
|
||||
? centerX - barWidth / 2
|
||||
: centerX - barWidth / 2 * (1 - j / (sets.length - 1))
|
||||
|
||||
ctx.fillRect(
|
||||
xLeft,
|
||||
|
|
|
@ -10,9 +10,8 @@ export function parseAlerta(string) {
|
|||
if (match[m]) {
|
||||
properties.push({
|
||||
name: match[m],
|
||||
args: match[m] === 'services'
|
||||
? match[m + 1].split(' ')
|
||||
: [match[m + 1]],
|
||||
args:
|
||||
match[m] === 'services' ? match[m + 1].split(' ') : [match[m + 1]],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,11 @@ const NavBar = React.createClass({
|
|||
|
||||
return child
|
||||
})
|
||||
return <nav className="sidebar">{children}</nav>
|
||||
return (
|
||||
<nav className="sidebar">
|
||||
{children}
|
||||
</nav>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ const RenameCluster = React.createClass({
|
|||
placeholder="Name this cluster..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -133,11 +133,15 @@ const InfluxTable = ({
|
|||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={s.id === source.id ? 'link-success' : null}
|
||||
>
|
||||
<strong>{s.name}</strong>
|
||||
<strong>
|
||||
{s.name}
|
||||
</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
</h5>
|
||||
<span>{s.url}</span>
|
||||
<span>
|
||||
{s.url}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<a
|
||||
|
|
|
@ -161,7 +161,9 @@ export const SourceForm = React.createClass({
|
|||
/>
|
||||
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
|
||||
</div>
|
||||
<label className="form-helper">{insecureSkipVerifyText}</label>
|
||||
<label className="form-helper">
|
||||
{insecureSkipVerifyText}
|
||||
</label>
|
||||
</div>
|
||||
: null}
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
|
|
|
@ -73,7 +73,9 @@ class ManageSources extends Component {
|
|||
setActiveKapacitor={setActiveKapacitor}
|
||||
handleDeleteKapacitor={deleteKapacitor}
|
||||
/>
|
||||
<p className="version-number">Version: {V_NUMBER}</p>
|
||||
<p className="version-number">
|
||||
Version: {V_NUMBER}
|
||||
</p>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
|
|
|
@ -19,9 +19,9 @@ class GettingStarted extends Component {
|
|||
</div>
|
||||
<div className="getting-started--cell">
|
||||
<p>
|
||||
<strong>Install the TICK Stack</strong><br />Save some time and
|
||||
use this handy tool to install the rest of the
|
||||
stack:
|
||||
<strong>Install the TICK Stack</strong>
|
||||
<br />Save some time and use this handy tool to install the rest
|
||||
of the stack:
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/influxdata/sandbox" target="_blank">
|
||||
|
@ -30,7 +30,9 @@ class GettingStarted extends Component {
|
|||
</p>
|
||||
</div>
|
||||
<div className="getting-started--cell">
|
||||
<p><strong>Guides</strong></p>
|
||||
<p>
|
||||
<strong>Guides</strong>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://docs.influxdata.com/chronograf/latest/guides/create-a-dashboard/"
|
||||
|
@ -76,7 +78,9 @@ class GettingStarted extends Component {
|
|||
</p>
|
||||
</div>
|
||||
<div className="getting-started--cell">
|
||||
<p><strong>Questions & Comments</strong></p>
|
||||
<p>
|
||||
<strong>Questions & Comments</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you have any product feedback please open a GitHub issue and
|
||||
we'll take a look. For any questions or other issues try posting
|
||||
|
|
|
@ -22,13 +22,19 @@ const JSONFeedReader = ({data}) =>
|
|||
</div>
|
||||
<div className="newsfeed--post-title">
|
||||
<a href={url} target="_blank">
|
||||
<h6>{title}</h6>
|
||||
<h6>
|
||||
{title}
|
||||
</h6>
|
||||
</a>
|
||||
<span>by {name}</span>
|
||||
<span>
|
||||
by {name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="newsfeed--content">
|
||||
{image ? <img src={image} /> : null}
|
||||
<p>{contentText}</p>
|
||||
<p>
|
||||
{contentText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -60,7 +60,11 @@ export const toggleField = (query, {field, funcs}, isKapacitorRule = false) => {
|
|||
|
||||
// all fields implicitly have a function applied to them, so consequently
|
||||
// we need to set the auto group by time
|
||||
export const toggleFieldWithGroupByInterval = (query, {field, funcs}, isKapacitorRule) => {
|
||||
export const toggleFieldWithGroupByInterval = (
|
||||
query,
|
||||
{field, funcs},
|
||||
isKapacitorRule
|
||||
) => {
|
||||
const queryWithField = toggleField(query, {field, funcs}, isKapacitorRule)
|
||||
return groupByTime(queryWithField, DEFAULT_DASHBOARD_GROUP_BY_INTERVAL)
|
||||
}
|
||||
|
@ -79,7 +83,11 @@ export function toggleTagAcceptance(query) {
|
|||
})
|
||||
}
|
||||
|
||||
export function applyFuncsToField(query, {field, funcs}, isInDataExplorer = false) {
|
||||
export function applyFuncsToField(
|
||||
query,
|
||||
{field, funcs},
|
||||
isInDataExplorer = false
|
||||
) {
|
||||
const shouldRemoveFuncs = funcs.length === 0
|
||||
const nextFields = query.fields.map(f => {
|
||||
// If one field has no funcs, all fields must have no funcs
|
||||
|
@ -95,9 +103,13 @@ export function applyFuncsToField(query, {field, funcs}, isInDataExplorer = fals
|
|||
return f
|
||||
})
|
||||
|
||||
const defaultGroupBy = isInDataExplorer ? DEFAULT_DATA_EXPLORER_GROUP_BY_INTERVAL : DEFAULT_DASHBOARD_GROUP_BY_INTERVAL
|
||||
const defaultGroupBy = isInDataExplorer
|
||||
? DEFAULT_DATA_EXPLORER_GROUP_BY_INTERVAL
|
||||
: DEFAULT_DASHBOARD_GROUP_BY_INTERVAL
|
||||
// If there are no functions, then there should be no GROUP BY time
|
||||
const nextGroupBy = Object.assign({}, query.groupBy, {time: shouldRemoveFuncs ? null : defaultGroupBy})
|
||||
const nextGroupBy = Object.assign({}, query.groupBy, {
|
||||
time: shouldRemoveFuncs ? null : defaultGroupBy,
|
||||
})
|
||||
|
||||
return Object.assign({}, query, {
|
||||
fields: nextFields,
|
||||
|
|
|
@ -5664,6 +5664,10 @@ preserve@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
|
||||
prettier@^1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.5.3.tgz#59dadc683345ec6b88f88b94ed4ae7e1da394bfe"
|
||||
|
||||
pretty-error@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.0.2.tgz#a7db19cbb529ca9f0af3d3a2f77d5caf8e5dec23"
|
||||
|
|
Loading…
Reference in New Issue