Merge branch 'master' into feature/pushover_support-1680

pull/10616/head
Jared Scheib 2017-07-20 16:23:54 -07:00
commit 200b24788b
91 changed files with 4881 additions and 4674 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

41
influx/jwt.go Normal file
View File

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

44
influx/jwt_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,11 +19,7 @@ export const KapacitorTasksPage = React.createClass({
},
render() {
return (
<div className="kapacitorTasks">
tasks
</div>
)
return <div className="kapacitorTasks">tasks</div>
},
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,8 @@ class Notifications extends Component {
})
return (
<div className={cls} role="alert">
{message}{this.renderDismiss(type)}
{message}
{this.renderDismiss(type)}
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -126,7 +126,11 @@ const NavBar = React.createClass({
return child
})
return <nav className="sidebar">{children}</nav>
return (
<nav className="sidebar">
{children}
</nav>
)
},
})

View File

@ -60,7 +60,6 @@ const RenameCluster = React.createClass({
placeholder="Name this cluster..."
/>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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