Merge pull request #3460 from influxdata/ifql/get-data

Ifql/get data
pull/3501/head
Andrew Watkins 2018-05-21 12:59:31 -07:00 committed by GitHub
commit b778fd9756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 3248 additions and 854 deletions

View File

@ -57,6 +57,9 @@
1. [#3412](https://github.com/influxdata/chronograf/pull/3412): Limit max-width of TICKScript editor 1. [#3412](https://github.com/influxdata/chronograf/pull/3412): Limit max-width of TICKScript editor
1. [#3166](https://github.com/influxdata/chronograf/pull/3166): Fix naming of new TICKScripts 1. [#3166](https://github.com/influxdata/chronograf/pull/3166): Fix naming of new TICKScripts
1. [#3449](https://github.com/influxdata/chronograf/pull/3449): Fix data explorer query error reporting regression 1. [#3449](https://github.com/influxdata/chronograf/pull/3449): Fix data explorer query error reporting regression
1. [#3412](https://github.com/influxdata/chronograf/pull/3412): Limit max-width of TICKScript editor.
1. [#3166](https://github.com/influxdata/chronograf/pull/3166): Fixes naming of new TICKScripts
1. [#3449](https://github.com/influxdata/chronograf/pull/3449): Fixes data explorer query error reporting regression
1. [#3453](https://github.com/influxdata/chronograf/pull/3453): Fix Kapacitor Logs fetch regression 1. [#3453](https://github.com/influxdata/chronograf/pull/3453): Fix Kapacitor Logs fetch regression
## v1.4.4.1 [2018-04-16] ## v1.4.4.1 [2018-04-16]

View File

@ -76,6 +76,14 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error {
// MarshalServer encodes a server to binary protobuf format. // MarshalServer encodes a server to binary protobuf format.
func MarshalServer(s chronograf.Server) ([]byte, error) { func MarshalServer(s chronograf.Server) ([]byte, error) {
var (
metadata []byte
err error
)
metadata, err = json.Marshal(s.Metadata)
if err != nil {
return nil, err
}
return proto.Marshal(&Server{ return proto.Marshal(&Server{
ID: int64(s.ID), ID: int64(s.ID),
SrcID: int64(s.SrcID), SrcID: int64(s.SrcID),
@ -86,6 +94,8 @@ func MarshalServer(s chronograf.Server) ([]byte, error) {
Active: s.Active, Active: s.Active,
Organization: s.Organization, Organization: s.Organization,
InsecureSkipVerify: s.InsecureSkipVerify, InsecureSkipVerify: s.InsecureSkipVerify,
Type: s.Type,
MetadataJSON: string(metadata),
}) })
} }
@ -96,6 +106,13 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
return err return err
} }
s.Metadata = make(map[string]interface{})
if len(pb.MetadataJSON) > 0 {
if err := json.Unmarshal([]byte(pb.MetadataJSON), &s.Metadata); err != nil {
return err
}
}
s.ID = int(pb.ID) s.ID = int(pb.ID)
s.SrcID = int(pb.SrcID) s.SrcID = int(pb.SrcID)
s.Name = pb.Name s.Name = pb.Name
@ -105,6 +122,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
s.Active = pb.Active s.Active = pb.Active
s.Organization = pb.Organization s.Organization = pb.Organization
s.InsecureSkipVerify = pb.InsecureSkipVerify s.InsecureSkipVerify = pb.InsecureSkipVerify
s.Type = pb.Type
return nil return nil
} }

View File

@ -734,6 +734,8 @@ type Server struct {
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"` Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"` Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"`
InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"`
Type string `protobuf:"bytes,10,opt,name=Type,proto3" json:"Type,omitempty"`
MetadataJSON string `protobuf:"bytes,11,opt,name=MetadataJSON,proto3" json:"MetadataJSON,omitempty"`
} }
func (m *Server) Reset() { *m = Server{} } func (m *Server) Reset() { *m = Server{} }
@ -804,6 +806,20 @@ func (m *Server) GetInsecureSkipVerify() bool {
return false return false
} }
func (m *Server) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Server) GetMetadataJSON() string {
if m != nil {
return m.MetadataJSON
}
return ""
}
type Layout struct { type Layout struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"` Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"`
@ -1372,110 +1388,111 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{ var fileDescriptorInternal = []byte{
// 1667 bytes of a gzipped FileDescriptorProto // 1685 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x5b, 0x6f, 0xe4, 0x48, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x5b, 0x8f, 0xe3, 0x48,
0x15, 0x96, 0xbb, 0xed, 0x4e, 0xfb, 0x74, 0x92, 0x8d, 0x8a, 0xd1, 0xae, 0x59, 0x10, 0x6a, 0x2c, 0x15, 0x96, 0x93, 0x38, 0x89, 0x4f, 0xd2, 0xbd, 0x2d, 0x33, 0xda, 0x35, 0x0b, 0x42, 0xc1, 0xe2,
0x2e, 0xe1, 0xb2, 0xc3, 0x2a, 0x2b, 0x24, 0xb4, 0xda, 0x5d, 0x29, 0x97, 0x9d, 0x21, 0x73, 0xcd, 0xd2, 0x5c, 0x76, 0x58, 0xf5, 0x0a, 0x09, 0xad, 0x76, 0x57, 0xea, 0xcb, 0xce, 0xd0, 0x73, 0xed,
0x54, 0x27, 0xc3, 0x13, 0x5a, 0x55, 0xdb, 0xd5, 0xdd, 0xa5, 0x75, 0xdb, 0xa6, 0x6c, 0x27, 0x31, 0xa9, 0x74, 0x0f, 0x4f, 0x68, 0x55, 0xb1, 0x2b, 0x49, 0x69, 0x1d, 0xdb, 0x94, 0xcb, 0xdd, 0x6d,
0xcf, 0xfc, 0x0e, 0x24, 0x24, 0xf8, 0x03, 0x88, 0x47, 0x24, 0xde, 0xf9, 0x01, 0xfc, 0x15, 0x78, 0x9e, 0xf9, 0x1d, 0x48, 0x48, 0xf0, 0x8e, 0x10, 0x8f, 0x48, 0xbc, 0xf3, 0x03, 0xf8, 0x2b, 0xbc,
0x44, 0xa7, 0x2e, 0xee, 0x72, 0xd2, 0x33, 0x1a, 0x24, 0xb4, 0x6f, 0xf5, 0x9d, 0x73, 0xfa, 0x54, 0xa2, 0x53, 0x17, 0xa7, 0xdc, 0x9d, 0x19, 0x0d, 0x12, 0xda, 0xb7, 0xfa, 0xce, 0x39, 0x39, 0x55,
0xd5, 0xb9, 0x7c, 0x75, 0xdc, 0xb0, 0x2f, 0xf2, 0x9a, 0xcb, 0x9c, 0x65, 0x0f, 0x4b, 0x59, 0xd4, 0x75, 0x2e, 0x5f, 0x1d, 0x07, 0xf6, 0x79, 0x2e, 0x99, 0xc8, 0x69, 0xf6, 0xb0, 0x14, 0x85, 0x2c,
0x05, 0x19, 0x5b, 0x1c, 0xff, 0x61, 0x08, 0xa3, 0x59, 0xd1, 0xc8, 0x84, 0x93, 0x7d, 0x18, 0x9c, 0xc2, 0xb1, 0xc5, 0xf1, 0x1f, 0xfa, 0x30, 0x9c, 0x17, 0xb5, 0x48, 0x58, 0xb8, 0x0f, 0xbd, 0xf3,
0x9f, 0x45, 0xde, 0xd4, 0x3b, 0x1c, 0xd2, 0xc1, 0xf9, 0x19, 0x21, 0xe0, 0xbf, 0x60, 0x6b, 0x1e, 0xb3, 0xc8, 0x9b, 0x79, 0x87, 0x7d, 0xd2, 0x3b, 0x3f, 0x0b, 0x43, 0x18, 0xbc, 0xa0, 0x1b, 0x16,
0x0d, 0xa6, 0xde, 0x61, 0x48, 0xd5, 0x1a, 0x65, 0x97, 0x6d, 0xc9, 0xa3, 0xa1, 0x96, 0xe1, 0x9a, 0xf5, 0x66, 0xde, 0x61, 0x40, 0xd4, 0x1a, 0x65, 0x97, 0x4d, 0xc9, 0xa2, 0xbe, 0x96, 0xe1, 0x3a,
0x7c, 0x08, 0xe3, 0xab, 0x0a, 0xbd, 0xad, 0x79, 0xe4, 0x2b, 0x79, 0x87, 0x51, 0x77, 0xc1, 0xaa, 0xfc, 0x10, 0xc6, 0x57, 0x15, 0x7a, 0xdb, 0xb0, 0x68, 0xa0, 0xe4, 0x2d, 0x46, 0xdd, 0x05, 0xad,
0xea, 0xa6, 0x90, 0x69, 0x14, 0x68, 0x9d, 0xc5, 0xe4, 0x00, 0x86, 0x57, 0xf4, 0x59, 0x34, 0x52, 0xaa, 0x9b, 0x42, 0xa4, 0x91, 0xaf, 0x75, 0x16, 0x87, 0x07, 0xd0, 0xbf, 0x22, 0xcf, 0xa2, 0xa1,
0x62, 0x5c, 0x92, 0x08, 0x76, 0xce, 0xf8, 0x82, 0x35, 0x59, 0x1d, 0xed, 0x4c, 0xbd, 0xc3, 0x31, 0x12, 0xe3, 0x32, 0x8c, 0x60, 0x74, 0xc6, 0x96, 0xb4, 0xce, 0x64, 0x34, 0x9a, 0x79, 0x87, 0x63,
0xb5, 0x10, 0xfd, 0x5c, 0xf2, 0x8c, 0x2f, 0x25, 0x5b, 0x44, 0x63, 0xed, 0xc7, 0x62, 0xf2, 0x10, 0x62, 0x21, 0xfa, 0xb9, 0x64, 0x19, 0x5b, 0x09, 0xba, 0x8c, 0xc6, 0xda, 0x8f, 0xc5, 0xe1, 0x43,
0xc8, 0x79, 0x5e, 0xf1, 0xa4, 0x91, 0x7c, 0xf6, 0xb5, 0x28, 0x5f, 0x73, 0x29, 0x16, 0x6d, 0x14, 0x08, 0xcf, 0xf3, 0x8a, 0x25, 0xb5, 0x60, 0xf3, 0xaf, 0x79, 0xf9, 0x9a, 0x09, 0xbe, 0x6c, 0xa2,
0x2a, 0x07, 0x5b, 0x34, 0xb8, 0xcb, 0x73, 0x5e, 0x33, 0xdc, 0x1b, 0x94, 0x2b, 0x0b, 0x49, 0x0c, 0x40, 0x39, 0xd8, 0xa1, 0xc1, 0x5d, 0x9e, 0x33, 0x49, 0x71, 0x6f, 0x50, 0xae, 0x2c, 0x0c, 0x63,
0xbb, 0xb3, 0x15, 0x93, 0x3c, 0x9d, 0xf1, 0x44, 0xf2, 0x3a, 0x9a, 0x28, 0x75, 0x4f, 0x86, 0x36, 0x98, 0xce, 0xd7, 0x54, 0xb0, 0x74, 0xce, 0x12, 0xc1, 0x64, 0x34, 0x51, 0xea, 0x8e, 0x0c, 0x6d,
0x2f, 0xe5, 0x92, 0xe5, 0xe2, 0xf7, 0xac, 0x16, 0x45, 0x1e, 0xed, 0x6a, 0x1b, 0x57, 0x86, 0x51, 0x5e, 0x8a, 0x15, 0xcd, 0xf9, 0xef, 0xa9, 0xe4, 0x45, 0x1e, 0x4d, 0xb5, 0x8d, 0x2b, 0xc3, 0x28,
0xa2, 0x45, 0xc6, 0xa3, 0x3d, 0x1d, 0x25, 0x5c, 0x93, 0xef, 0x42, 0x68, 0x2e, 0x43, 0x2f, 0xa2, 0x91, 0x22, 0x63, 0xd1, 0x9e, 0x8e, 0x12, 0xae, 0xc3, 0xef, 0x42, 0x60, 0x2e, 0x43, 0x2e, 0xa2,
0x7d, 0xa5, 0xd8, 0x08, 0xe2, 0xbf, 0x79, 0x10, 0x9e, 0xb1, 0x6a, 0x35, 0x2f, 0x98, 0x4c, 0xdf, 0x7d, 0xa5, 0xd8, 0x0a, 0xe2, 0xbf, 0x7b, 0x10, 0x9c, 0xd1, 0x6a, 0xbd, 0x28, 0xa8, 0x48, 0xdf,
0x29, 0x13, 0x1f, 0x41, 0x90, 0xf0, 0x2c, 0xab, 0xa2, 0xe1, 0x74, 0x78, 0x38, 0x39, 0xfa, 0xe0, 0x29, 0x13, 0x1f, 0x81, 0x9f, 0xb0, 0x2c, 0xab, 0xa2, 0xfe, 0xac, 0x7f, 0x38, 0x39, 0xfa, 0xe0,
0x61, 0x97, 0xe2, 0xce, 0xcf, 0x29, 0xcf, 0x32, 0xaa, 0xad, 0xc8, 0xc7, 0x10, 0xd6, 0x7c, 0x5d, 0x61, 0x9b, 0xe2, 0xd6, 0xcf, 0x29, 0xcb, 0x32, 0xa2, 0xad, 0xc2, 0x8f, 0x21, 0x90, 0x6c, 0x53,
0x66, 0xac, 0xe6, 0x55, 0xe4, 0xab, 0x9f, 0x90, 0xcd, 0x4f, 0x2e, 0x8d, 0x8a, 0x6e, 0x8c, 0xee, 0x66, 0x54, 0xb2, 0x2a, 0x1a, 0xa8, 0x9f, 0x84, 0xdb, 0x9f, 0x5c, 0x1a, 0x15, 0xd9, 0x1a, 0xdd,
0x5d, 0x34, 0xb8, 0x7f, 0xd1, 0xf8, 0x5f, 0x3e, 0xec, 0xf5, 0xb6, 0x23, 0xbb, 0xe0, 0xdd, 0xaa, 0xbb, 0xa8, 0x7f, 0xff, 0xa2, 0xf1, 0xbf, 0x07, 0xb0, 0xd7, 0xd9, 0x2e, 0x9c, 0x82, 0x77, 0xab,
0x93, 0x07, 0xd4, 0xbb, 0x45, 0xd4, 0xaa, 0x53, 0x07, 0xd4, 0x6b, 0x11, 0xdd, 0xa8, 0xca, 0x09, 0x4e, 0xee, 0x13, 0xef, 0x16, 0x51, 0xa3, 0x4e, 0xed, 0x13, 0xaf, 0x41, 0x74, 0xa3, 0x2a, 0xc7,
0xa8, 0x77, 0x83, 0x68, 0xa5, 0xea, 0x25, 0xa0, 0xde, 0x8a, 0xfc, 0x04, 0x76, 0x7e, 0xd7, 0x70, 0x27, 0xde, 0x0d, 0xa2, 0xb5, 0xaa, 0x17, 0x9f, 0x78, 0xeb, 0xf0, 0x27, 0x30, 0xfa, 0x5d, 0xcd,
0x29, 0x78, 0x15, 0x05, 0xea, 0x74, 0xef, 0x6d, 0x4e, 0xf7, 0xaa, 0xe1, 0xb2, 0xa5, 0x56, 0x8f, 0x04, 0x67, 0x55, 0xe4, 0xab, 0xd3, 0xbd, 0xb7, 0x3d, 0xdd, 0xab, 0x9a, 0x89, 0x86, 0x58, 0x3d,
0xd1, 0x50, 0xb5, 0xa6, 0x0b, 0x47, 0xad, 0x51, 0x56, 0x63, 0x5d, 0xee, 0x68, 0x19, 0xae, 0x4d, 0x46, 0x43, 0xd5, 0x9a, 0x2e, 0x1c, 0xb5, 0x46, 0x99, 0xc4, 0xba, 0x1c, 0x69, 0x19, 0xae, 0x4d,
0x14, 0x75, 0xb5, 0x60, 0x14, 0x7f, 0x09, 0x3e, 0xbb, 0xe5, 0x55, 0x14, 0x2a, 0xff, 0xdf, 0x7f, 0x14, 0x75, 0xb5, 0x60, 0x14, 0x7f, 0x09, 0x03, 0x7a, 0xcb, 0xaa, 0x28, 0x50, 0xfe, 0xbf, 0xff,
0x43, 0xc0, 0x1e, 0x1e, 0xdf, 0xf2, 0xea, 0xcb, 0xbc, 0x96, 0x2d, 0x55, 0xe6, 0xe4, 0xc7, 0x30, 0x86, 0x80, 0x3d, 0x3c, 0xbe, 0x65, 0xd5, 0x97, 0xb9, 0x14, 0x0d, 0x51, 0xe6, 0xe1, 0x8f, 0x61,
0x4a, 0x8a, 0xac, 0x90, 0x55, 0x04, 0x77, 0x0f, 0x76, 0x8a, 0x72, 0x6a, 0xd4, 0xe4, 0x10, 0x46, 0x98, 0x14, 0x59, 0x21, 0xaa, 0x08, 0xee, 0x1e, 0xec, 0x14, 0xe5, 0xc4, 0xa8, 0xc3, 0x43, 0x18,
0x19, 0x5f, 0xf2, 0x3c, 0x55, 0x75, 0x33, 0x39, 0x3a, 0xd8, 0x18, 0x3e, 0x53, 0x72, 0x6a, 0xf4, 0x66, 0x6c, 0xc5, 0xf2, 0x54, 0xd5, 0xcd, 0xe4, 0xe8, 0x60, 0x6b, 0xf8, 0x4c, 0xc9, 0x89, 0xd1,
0xe4, 0x53, 0xd8, 0xad, 0xd9, 0x3c, 0xe3, 0x2f, 0x4b, 0x8c, 0x62, 0xa5, 0x6a, 0x68, 0x72, 0xf4, 0x87, 0x9f, 0xc2, 0x54, 0xd2, 0x45, 0xc6, 0x5e, 0x96, 0x18, 0xc5, 0x4a, 0xd5, 0xd0, 0xe4, 0xe8,
0xbe, 0x93, 0x0f, 0x47, 0x4b, 0x7b, 0xb6, 0xe4, 0x33, 0xd8, 0x5d, 0x08, 0x9e, 0xa5, 0xf6, 0xb7, 0x7d, 0x27, 0x1f, 0x8e, 0x96, 0x74, 0x6c, 0xc3, 0xcf, 0x60, 0xba, 0xe4, 0x2c, 0x4b, 0xed, 0x6f,
0x7b, 0xea, 0x50, 0xd1, 0xe6, 0xb7, 0x94, 0xe7, 0x6c, 0x8d, 0xbf, 0x78, 0x84, 0x66, 0xb4, 0x67, 0xf7, 0xd4, 0xa1, 0xa2, 0xed, 0x6f, 0x09, 0xcb, 0xe9, 0x06, 0x7f, 0xf1, 0x08, 0xcd, 0x48, 0xc7,
0x4d, 0xbe, 0x07, 0x50, 0x8b, 0x35, 0x7f, 0x54, 0xc8, 0x35, 0xab, 0x4d, 0x19, 0x3a, 0x12, 0xf2, 0x3a, 0xfc, 0x1e, 0x80, 0xe4, 0x1b, 0xf6, 0xa8, 0x10, 0x1b, 0x2a, 0x4d, 0x19, 0x3a, 0x92, 0xf0,
0x39, 0xec, 0xa5, 0x3c, 0x11, 0x6b, 0x96, 0x5d, 0x64, 0x2c, 0xe1, 0x55, 0xf4, 0x9e, 0x3a, 0x9a, 0x73, 0xd8, 0x4b, 0x59, 0xc2, 0x37, 0x34, 0xbb, 0xc8, 0x68, 0xc2, 0xaa, 0xe8, 0x3d, 0x75, 0x34,
0x5b, 0x5d, 0xae, 0x9a, 0xf6, 0xad, 0x3f, 0x7c, 0x0c, 0x61, 0x17, 0x3e, 0xec, 0xef, 0xaf, 0x79, 0xb7, 0xba, 0x5c, 0x35, 0xe9, 0x5a, 0x7f, 0xf8, 0x18, 0x82, 0x36, 0x7c, 0xd8, 0xdf, 0x5f, 0xb3,
0xab, 0x8a, 0x21, 0xa4, 0xb8, 0x24, 0x3f, 0x80, 0xe0, 0x9a, 0x65, 0x8d, 0x2e, 0xe4, 0xc9, 0xd1, 0x46, 0x15, 0x43, 0x40, 0x70, 0x19, 0xfe, 0x00, 0xfc, 0x6b, 0x9a, 0xd5, 0xba, 0x90, 0x27, 0x47,
0xfe, 0xc6, 0xeb, 0xf1, 0xad, 0xa8, 0xa8, 0x56, 0x7e, 0x3a, 0xf8, 0x95, 0x17, 0x3f, 0x86, 0xbd, 0xfb, 0x5b, 0xaf, 0xc7, 0xb7, 0xbc, 0x22, 0x5a, 0xf9, 0x69, 0xef, 0x57, 0x5e, 0xfc, 0x18, 0xf6,
0xde, 0x46, 0x78, 0x70, 0x51, 0x7d, 0x99, 0x2f, 0x0a, 0x99, 0xf0, 0x54, 0xf9, 0x1c, 0x53, 0x47, 0x3a, 0x1b, 0xe1, 0xc1, 0x79, 0xf5, 0x65, 0xbe, 0x2c, 0x44, 0xc2, 0x52, 0xe5, 0x73, 0x4c, 0x1c,
0x42, 0xde, 0x87, 0x51, 0x2a, 0x96, 0xa2, 0xae, 0x4c, 0xb9, 0x19, 0x14, 0xff, 0xdd, 0x83, 0x5d, 0x49, 0xf8, 0x3e, 0x0c, 0x53, 0xbe, 0xe2, 0xb2, 0x32, 0xe5, 0x66, 0x50, 0xfc, 0x0f, 0x0f, 0xa6,
0x37, 0x9a, 0xe4, 0xa7, 0x70, 0x70, 0xcd, 0x65, 0x2d, 0x12, 0x96, 0x5d, 0x8a, 0x35, 0xc7, 0x8d, 0x6e, 0x34, 0xc3, 0x9f, 0xc2, 0xc1, 0x35, 0x13, 0x92, 0x27, 0x34, 0xbb, 0xe4, 0x1b, 0x86, 0x1b,
0xd5, 0x4f, 0xc6, 0xf4, 0x9e, 0x9c, 0x7c, 0x0c, 0xa3, 0xaa, 0x90, 0xf5, 0x49, 0xab, 0xaa, 0xf6, 0xab, 0x9f, 0x8c, 0xc9, 0x3d, 0x79, 0xf8, 0x31, 0x0c, 0xab, 0x42, 0xc8, 0x93, 0x46, 0x55, 0xed,
0x6d, 0x51, 0x36, 0x76, 0xc8, 0x53, 0x37, 0x92, 0x95, 0xa5, 0xc8, 0x97, 0x96, 0x0b, 0x2d, 0x26, 0xdb, 0xa2, 0x6c, 0xec, 0x90, 0xa7, 0x6e, 0x04, 0x2d, 0x4b, 0x9e, 0xaf, 0x2c, 0x17, 0x5a, 0x1c,
0x3f, 0x82, 0xfd, 0x85, 0xb8, 0x7d, 0x24, 0x64, 0x55, 0x9f, 0x16, 0x59, 0xb3, 0xce, 0x55, 0x05, 0xfe, 0x08, 0xf6, 0x97, 0xfc, 0xf6, 0x11, 0x17, 0x95, 0x3c, 0x2d, 0xb2, 0x7a, 0x93, 0xab, 0x0a,
0x8f, 0xe9, 0x1d, 0xe9, 0x13, 0x7f, 0xec, 0x1d, 0x0c, 0x9e, 0xf8, 0xe3, 0xe0, 0x60, 0x14, 0x97, 0x1e, 0x93, 0x3b, 0xd2, 0x27, 0x83, 0xb1, 0x77, 0xd0, 0x7b, 0x32, 0x18, 0xfb, 0x07, 0xc3, 0xb8,
0xb0, 0xdf, 0xdf, 0x09, 0xdb, 0xd2, 0x1e, 0x42, 0x71, 0x82, 0x0e, 0x6f, 0x4f, 0x46, 0xa6, 0x30, 0x84, 0xfd, 0xee, 0x4e, 0xd8, 0x96, 0xf6, 0x10, 0x8a, 0x13, 0x74, 0x78, 0x3b, 0xb2, 0x70, 0x06,
0x49, 0x45, 0x55, 0x66, 0xac, 0x75, 0x68, 0xc3, 0x15, 0x21, 0x07, 0x5e, 0x8b, 0x4a, 0xcc, 0x33, 0x93, 0x94, 0x57, 0x65, 0x46, 0x1b, 0x87, 0x36, 0x5c, 0x11, 0x72, 0xe0, 0x35, 0xaf, 0xf8, 0x22,
0x4d, 0xe5, 0x63, 0x6a, 0x61, 0xbc, 0x84, 0x40, 0x95, 0xb5, 0x43, 0x42, 0xa1, 0x25, 0x21, 0x45, 0xd3, 0x54, 0x3e, 0x26, 0x16, 0xc6, 0x2b, 0xf0, 0x55, 0x59, 0x3b, 0x24, 0x14, 0x58, 0x12, 0x52,
0xfd, 0x03, 0x87, 0xfa, 0x0f, 0x60, 0xf8, 0x6b, 0x7e, 0x6b, 0x5e, 0x03, 0x5c, 0x76, 0x54, 0xe5, 0xd4, 0xdf, 0x73, 0xa8, 0xff, 0x00, 0xfa, 0xbf, 0x66, 0xb7, 0xe6, 0x35, 0xc0, 0x65, 0x4b, 0x55,
0x3b, 0x54, 0xf5, 0x00, 0x82, 0xd7, 0x2a, 0xed, 0x9a, 0x42, 0x34, 0x88, 0xbf, 0x80, 0x91, 0x6e, 0x03, 0x87, 0xaa, 0x1e, 0x80, 0xff, 0x5a, 0xa5, 0x5d, 0x53, 0x88, 0x06, 0xf1, 0x17, 0x30, 0xd4,
0x8b, 0xce, 0xb3, 0xe7, 0x78, 0x9e, 0xc2, 0xe4, 0xa5, 0x14, 0x3c, 0xaf, 0x35, 0xf9, 0x98, 0x2b, 0x6d, 0xd1, 0x7a, 0xf6, 0x1c, 0xcf, 0x33, 0x98, 0xbc, 0x14, 0x9c, 0xe5, 0x52, 0x93, 0x8f, 0xb9,
0x38, 0xa2, 0xf8, 0xaf, 0x1e, 0xf8, 0x2a, 0x4b, 0x31, 0xec, 0x66, 0x7c, 0xc9, 0x92, 0xf6, 0xa4, 0x82, 0x23, 0x8a, 0xff, 0xe6, 0xc1, 0x40, 0x65, 0x29, 0x86, 0x69, 0xc6, 0x56, 0x34, 0x69, 0x4e,
0x68, 0xf2, 0xb4, 0x8a, 0xbc, 0xe9, 0xf0, 0x70, 0x48, 0x7b, 0x32, 0x2c, 0x8f, 0xb9, 0xd6, 0x0e, 0x8a, 0x3a, 0x4f, 0xab, 0xc8, 0x9b, 0xf5, 0x0f, 0xfb, 0xa4, 0x23, 0xc3, 0xf2, 0x58, 0x68, 0x6d,
0xa6, 0xc3, 0xc3, 0x90, 0x1a, 0x84, 0x47, 0xcb, 0xd8, 0x9c, 0x67, 0xe6, 0x0a, 0x1a, 0xa0, 0x75, 0x6f, 0xd6, 0x3f, 0x0c, 0x88, 0x41, 0x78, 0xb4, 0x8c, 0x2e, 0x58, 0x66, 0xae, 0xa0, 0x01, 0x5a,
0x29, 0xf9, 0x42, 0xdc, 0x9a, 0x6b, 0x18, 0x84, 0xf2, 0xaa, 0x59, 0xa0, 0x5c, 0xdf, 0xc4, 0x20, 0x97, 0x82, 0x2d, 0xf9, 0xad, 0xb9, 0x86, 0x41, 0x28, 0xaf, 0xea, 0x25, 0xca, 0xf5, 0x4d, 0x0c,
0xbc, 0xc0, 0x9c, 0x55, 0x1d, 0x23, 0xe1, 0x1a, 0x3d, 0x57, 0x09, 0xcb, 0x2c, 0x25, 0x69, 0x10, 0xc2, 0x0b, 0x2c, 0x68, 0xd5, 0x32, 0x12, 0xae, 0xd1, 0x73, 0x95, 0xd0, 0xcc, 0x52, 0x92, 0x06,
0xff, 0xc3, 0xc3, 0x87, 0x4c, 0x53, 0xec, 0xbd, 0x08, 0x7f, 0x1b, 0xc6, 0x48, 0xbf, 0x5f, 0x5d, 0xf1, 0x3f, 0x3d, 0x7c, 0xc8, 0x34, 0xc5, 0xde, 0x8b, 0xf0, 0xb7, 0x61, 0x8c, 0xf4, 0xfb, 0xd5,
0x33, 0x69, 0x2e, 0xbc, 0x83, 0xf8, 0x35, 0x93, 0xe4, 0x17, 0x30, 0x52, 0xcd, 0xb1, 0x85, 0xee, 0x35, 0x15, 0xe6, 0xc2, 0x23, 0xc4, 0xaf, 0xa9, 0x08, 0x7f, 0x01, 0x43, 0xd5, 0x1c, 0x3b, 0xe8,
0xad, 0x3b, 0x15, 0x55, 0x6a, 0xcc, 0x3a, 0x42, 0xf4, 0x1d, 0x42, 0xec, 0x2e, 0x1b, 0xb8, 0x97, 0xde, 0xba, 0x53, 0x51, 0x25, 0xc6, 0xac, 0x25, 0xc4, 0x81, 0x43, 0x88, 0xed, 0x65, 0x7d, 0xf7,
0xfd, 0x08, 0x02, 0x64, 0xd6, 0x56, 0x9d, 0x7e, 0xab, 0x67, 0xcd, 0xbf, 0xda, 0x2a, 0xbe, 0x82, 0xb2, 0x1f, 0x81, 0x8f, 0xcc, 0xda, 0xa8, 0xd3, 0xef, 0xf4, 0xac, 0xf9, 0x57, 0x5b, 0xc5, 0x57,
0xbd, 0xde, 0x8e, 0xdd, 0x4e, 0x5e, 0x7f, 0xa7, 0x4d, 0xa3, 0x87, 0xa6, 0xb1, 0xb1, 0x39, 0x2a, 0xb0, 0xd7, 0xd9, 0xb1, 0xdd, 0xc9, 0xeb, 0xee, 0xb4, 0x6d, 0xf4, 0xc0, 0x34, 0x36, 0x36, 0x47,
0x9e, 0xf1, 0xa4, 0xe6, 0xa9, 0xa9, 0xba, 0x0e, 0xc7, 0x7f, 0xf2, 0x36, 0x7e, 0xd5, 0x7e, 0x58, 0xc5, 0x32, 0x96, 0x48, 0x96, 0x9a, 0xaa, 0x6b, 0x71, 0xfc, 0x27, 0x6f, 0xeb, 0x57, 0xed, 0x87,
0xa2, 0x49, 0xb1, 0x5e, 0xb3, 0x3c, 0x35, 0xae, 0x2d, 0xc4, 0xb8, 0xa5, 0x73, 0xe3, 0x7a, 0x90, 0x25, 0x9a, 0x14, 0x9b, 0x0d, 0xcd, 0x53, 0xe3, 0xda, 0x42, 0x8c, 0x5b, 0xba, 0x30, 0xae, 0x7b,
0xce, 0x11, 0xcb, 0xd2, 0x64, 0x70, 0x20, 0x4b, 0xac, 0x9d, 0x35, 0x67, 0x55, 0x23, 0xf9, 0x9a, 0xe9, 0x02, 0xb1, 0x28, 0x4d, 0x06, 0x7b, 0xa2, 0xc4, 0xda, 0xd9, 0x30, 0x5a, 0xd5, 0x82, 0x6d,
0xe7, 0xb5, 0x09, 0x81, 0x2b, 0x22, 0x1f, 0xc0, 0x4e, 0xcd, 0x96, 0x5f, 0x21, 0x3d, 0x99, 0x4c, 0x58, 0x2e, 0x4d, 0x08, 0x5c, 0x51, 0xf8, 0x01, 0x8c, 0x24, 0x5d, 0x7d, 0x85, 0xf4, 0x64, 0x32,
0xd6, 0x6c, 0xf9, 0x94, 0xb7, 0xe4, 0x3b, 0x10, 0x2a, 0xbe, 0x54, 0x2a, 0x9d, 0xce, 0xb1, 0x12, 0x29, 0xe9, 0xea, 0x29, 0x6b, 0xc2, 0xef, 0x40, 0xa0, 0xf8, 0x52, 0xa9, 0x74, 0x3a, 0xc7, 0x4a,
0x3c, 0xe5, 0x6d, 0xfc, 0x1f, 0x0f, 0x46, 0x33, 0x2e, 0xaf, 0xb9, 0x7c, 0xa7, 0x17, 0xda, 0x9d, 0xf0, 0x94, 0x35, 0xf1, 0x5f, 0x7b, 0x30, 0x9c, 0x33, 0x71, 0xcd, 0xc4, 0x3b, 0xbd, 0xd0, 0xee,
0x8b, 0x86, 0x6f, 0x99, 0x8b, 0xfc, 0xed, 0x73, 0x51, 0xb0, 0x99, 0x8b, 0x1e, 0x40, 0x30, 0x93, 0x5c, 0xd4, 0x7f, 0xcb, 0x5c, 0x34, 0xd8, 0x3d, 0x17, 0xf9, 0xdb, 0xb9, 0xe8, 0x01, 0xf8, 0x73,
0xc9, 0xf9, 0x99, 0x3a, 0xd1, 0x90, 0x6a, 0x80, 0xd5, 0x78, 0x9c, 0xd4, 0xe2, 0x9a, 0x9b, 0x61, 0x91, 0x9c, 0x9f, 0xa9, 0x13, 0xf5, 0x89, 0x06, 0x58, 0x8d, 0xc7, 0x89, 0xe4, 0xd7, 0xcc, 0x0c,
0xc9, 0xa0, 0x7b, 0x0f, 0xf7, 0x78, 0xcb, 0x84, 0xf2, 0x3f, 0xce, 0x4c, 0xf1, 0x1f, 0x3d, 0x18, 0x4b, 0x06, 0xdd, 0x7b, 0xb8, 0xc7, 0x3b, 0x26, 0x94, 0xff, 0x75, 0x66, 0xb2, 0x2d, 0x0a, 0x4e,
0x3d, 0x63, 0x6d, 0xd1, 0xd4, 0xf7, 0xaa, 0x76, 0x0a, 0x93, 0xe3, 0xb2, 0xcc, 0x44, 0xd2, 0xeb, 0x8b, 0xc6, 0x30, 0xc5, 0xc1, 0x29, 0xa5, 0x92, 0x3e, 0x99, 0xbf, 0x7c, 0x61, 0xa7, 0x25, 0x57,
0x54, 0x47, 0x84, 0x16, 0xcf, 0x9d, 0x7c, 0xe8, 0x58, 0xb8, 0x22, 0x7c, 0x18, 0x4e, 0xd5, 0x30, 0x16, 0xff, 0xd1, 0x83, 0xe1, 0x33, 0xda, 0x14, 0xb5, 0xbc, 0x57, 0xed, 0x33, 0x98, 0x1c, 0x97,
0xa3, 0x27, 0x13, 0xe7, 0x61, 0xd0, 0x33, 0x8c, 0x52, 0x62, 0xd0, 0x8e, 0x9b, 0xba, 0x58, 0x64, 0x65, 0xc6, 0x93, 0x4e, 0x87, 0x3b, 0x22, 0xb4, 0x78, 0xee, 0xe4, 0x51, 0xc7, 0xd0, 0x15, 0xe1,
0xc5, 0x8d, 0x8a, 0xce, 0x98, 0x76, 0x38, 0xfe, 0xe7, 0x00, 0xfc, 0x6f, 0x6a, 0x00, 0xd9, 0x05, 0x83, 0x72, 0xaa, 0x86, 0x20, 0x3d, 0xd1, 0x38, 0x0f, 0x8a, 0x9e, 0x7d, 0x94, 0x12, 0x83, 0x7d,
0x4f, 0x98, 0xe2, 0xf0, 0x44, 0x37, 0x8e, 0xec, 0x38, 0xe3, 0x48, 0x04, 0x3b, 0xad, 0x64, 0xf9, 0x5c, 0xcb, 0x62, 0x99, 0x15, 0x37, 0x2a, 0xaa, 0x63, 0xd2, 0xe2, 0xf8, 0x5f, 0x3d, 0x18, 0x7c,
0x92, 0x57, 0xd1, 0x58, 0xb1, 0x91, 0x85, 0x4a, 0xa3, 0xfa, 0x4e, 0xcf, 0x21, 0x21, 0xb5, 0xb0, 0x53, 0x83, 0xcb, 0x14, 0x3c, 0x6e, 0x8a, 0xca, 0xe3, 0xed, 0x18, 0x33, 0x72, 0xc6, 0x98, 0x08,
0xeb, 0x23, 0x70, 0xfa, 0xe8, 0xe7, 0x66, 0x64, 0x99, 0xdc, 0x7d, 0xe4, 0xb7, 0x4d, 0x2a, 0xff, 0x46, 0x8d, 0xa0, 0xf9, 0x8a, 0x55, 0xd1, 0x58, 0xb1, 0x98, 0x85, 0x4a, 0xa3, 0xfa, 0x55, 0xcf,
0xbf, 0xd7, 0xf7, 0xdf, 0x1e, 0x04, 0x5d, 0x13, 0x9e, 0xf6, 0x9b, 0xf0, 0x74, 0xd3, 0x84, 0x67, 0x2f, 0x01, 0xb1, 0xb0, 0xed, 0x3f, 0x70, 0xfa, 0xef, 0xe7, 0x66, 0xd4, 0x99, 0xdc, 0x1d, 0x0e,
0x27, 0xb6, 0x09, 0xcf, 0x4e, 0x10, 0xd3, 0x0b, 0xdb, 0x84, 0xf4, 0x02, 0x93, 0xf5, 0x58, 0x16, 0x76, 0x4d, 0x38, 0xff, 0xbf, 0x57, 0xfb, 0x3f, 0x1e, 0xf8, 0x6d, 0xf3, 0x9e, 0x76, 0x9b, 0xf7,
0x4d, 0x79, 0xd2, 0xea, 0xac, 0x86, 0xb4, 0xc3, 0x58, 0xb9, 0xbf, 0x59, 0x71, 0x69, 0x42, 0x1d, 0x74, 0xdb, 0xbc, 0x67, 0x27, 0xb6, 0x79, 0xcf, 0x4e, 0x10, 0x93, 0x0b, 0xdb, 0xbc, 0xe4, 0x02,
0x52, 0x83, 0xb0, 0xce, 0x9f, 0x29, 0x82, 0xd2, 0xc1, 0xd5, 0x80, 0xfc, 0x10, 0x02, 0x8a, 0xc1, 0x93, 0xf5, 0x58, 0x14, 0x75, 0x79, 0xd2, 0xe8, 0xac, 0x06, 0xa4, 0xc5, 0x58, 0xf1, 0xbf, 0x59,
0x53, 0x11, 0xee, 0xe5, 0x45, 0x89, 0xa9, 0xd6, 0xa2, 0x53, 0xfd, 0x21, 0x63, 0x0a, 0xde, 0x7e, 0x33, 0x61, 0x42, 0x1d, 0x10, 0x83, 0xb0, 0x3f, 0x9e, 0x29, 0x62, 0xd3, 0xc1, 0xd5, 0x20, 0xfc,
0xd6, 0xfc, 0x0c, 0x46, 0xb3, 0x95, 0x58, 0xd4, 0x76, 0xf0, 0xfb, 0x96, 0x43, 0x70, 0x62, 0xcd, 0x21, 0xf8, 0x04, 0x83, 0xa7, 0x22, 0xdc, 0xc9, 0x8b, 0x12, 0x13, 0xad, 0x45, 0xa7, 0xfa, 0x03,
0x95, 0x8e, 0x1a, 0x93, 0xf8, 0x15, 0x84, 0x9d, 0x70, 0x73, 0x1c, 0xcf, 0x3d, 0x0e, 0x01, 0xff, 0xc8, 0x34, 0x8a, 0xfd, 0x1c, 0xfa, 0x19, 0x0c, 0xe7, 0x6b, 0xbe, 0x94, 0x76, 0x60, 0xfc, 0x96,
0x2a, 0x17, 0xb5, 0x6d, 0x75, 0x5c, 0xe3, 0x65, 0x5f, 0x35, 0x2c, 0xaf, 0x45, 0xdd, 0xda, 0x56, 0x43, 0x8c, 0x7c, 0xc3, 0x94, 0x8e, 0x18, 0x93, 0xf8, 0x15, 0x04, 0xad, 0x70, 0x7b, 0x1c, 0xcf,
0xb7, 0x38, 0xfe, 0xc4, 0x1c, 0x1f, 0xdd, 0x5d, 0x95, 0x25, 0x97, 0x86, 0x36, 0x34, 0x50, 0x9b, 0x3d, 0x4e, 0x08, 0x83, 0xab, 0x9c, 0x4b, 0x4b, 0x11, 0xb8, 0xc6, 0xcb, 0xbe, 0xaa, 0x69, 0x2e,
0x14, 0x37, 0x5c, 0x33, 0xfe, 0x90, 0x6a, 0x10, 0xff, 0x16, 0xc2, 0xe3, 0x8c, 0xcb, 0x9a, 0x36, 0xb9, 0x6c, 0x2c, 0x45, 0x58, 0x1c, 0x7f, 0x62, 0x8e, 0x8f, 0xee, 0xae, 0xca, 0x92, 0x09, 0x43,
0x19, 0xdf, 0xf6, 0x12, 0x3f, 0x99, 0xbd, 0x7c, 0x61, 0x4f, 0x80, 0xeb, 0x0d, 0x45, 0x0c, 0xef, 0x37, 0x1a, 0xa8, 0x4d, 0x8a, 0x1b, 0xa6, 0x5f, 0x8a, 0x3e, 0xd1, 0x20, 0xfe, 0x2d, 0x04, 0xc7,
0x50, 0xc4, 0x53, 0x56, 0xb2, 0xf3, 0x33, 0x55, 0xe7, 0x43, 0x6a, 0x50, 0xfc, 0x67, 0x0f, 0x7c, 0x19, 0x13, 0x92, 0xd4, 0x19, 0xdb, 0xf5, 0x82, 0xab, 0x46, 0x35, 0x27, 0xc0, 0xf5, 0x96, 0x5a,
0xe4, 0x22, 0xc7, 0xb5, 0xff, 0x36, 0x1e, 0xbb, 0x90, 0xc5, 0xb5, 0x48, 0xb9, 0xb4, 0x97, 0xb3, 0xfa, 0x77, 0xa8, 0xe5, 0x29, 0x2d, 0xe9, 0xf9, 0x99, 0xaa, 0xf3, 0x3e, 0x31, 0x28, 0xfe, 0xb3,
0x58, 0x05, 0x3d, 0x59, 0xf1, 0xee, 0xc1, 0x37, 0x08, 0x6b, 0x0d, 0xbf, 0x7a, 0x6c, 0x2f, 0x39, 0x07, 0x03, 0xe4, 0x30, 0xc7, 0xf5, 0xe0, 0x6d, 0xfc, 0x77, 0x21, 0x8a, 0x6b, 0x9e, 0x32, 0x61,
0xb5, 0x86, 0x62, 0xaa, 0x95, 0x38, 0xd4, 0xcd, 0x9a, 0x92, 0xcb, 0xe3, 0x74, 0x2d, 0xec, 0x34, 0x2f, 0x67, 0xb1, 0x0a, 0x7a, 0xb2, 0x66, 0xed, 0xa0, 0x60, 0x10, 0xd6, 0x1a, 0x7e, 0x2d, 0xd9,
0xe4, 0x48, 0xe2, 0x2f, 0xf4, 0x77, 0xd4, 0x3d, 0x46, 0xf3, 0xb6, 0x7f, 0x73, 0xdd, 0x3d, 0x79, 0x5e, 0x72, 0x6a, 0x0d, 0xc5, 0x44, 0x2b, 0x71, 0x18, 0x9c, 0xd7, 0x25, 0x13, 0xc7, 0xe9, 0x86,
0xfc, 0x17, 0x0f, 0x76, 0x9e, 0x9b, 0xe9, 0xcb, 0xbd, 0x85, 0xf7, 0xc6, 0x5b, 0x0c, 0x7a, 0xb7, 0xdb, 0x29, 0xca, 0x91, 0xc4, 0x5f, 0xe8, 0xef, 0xaf, 0x7b, 0x4c, 0xe8, 0xed, 0xfe, 0x56, 0xbb,
0x38, 0x82, 0x07, 0xd6, 0xa6, 0xb7, 0xbf, 0x8e, 0xc2, 0x56, 0x9d, 0x89, 0xa8, 0xdf, 0x25, 0xeb, 0x7b, 0xf2, 0xf8, 0x2f, 0x1e, 0x8c, 0x9e, 0x9b, 0xa9, 0xcd, 0xbd, 0x85, 0xf7, 0xc6, 0x5b, 0xf4,
0x5d, 0x3e, 0xa3, 0x2e, 0xfb, 0x36, 0xdb, 0x12, 0x7e, 0x2f, 0x2b, 0x53, 0x98, 0xd8, 0xcf, 0xc7, 0x3a, 0xb7, 0x38, 0x82, 0x07, 0xd6, 0xa6, 0xb3, 0xbf, 0x8e, 0xc2, 0x4e, 0x9d, 0x89, 0xe8, 0xa0,
0x22, 0xb3, 0x0f, 0x8c, 0x2b, 0x8a, 0x8f, 0x60, 0x74, 0x5a, 0xe4, 0x0b, 0xb1, 0x24, 0x87, 0xe0, 0x4d, 0xd6, 0xbb, 0x7c, 0x7e, 0x5d, 0x76, 0x6d, 0x76, 0x25, 0xfc, 0x5e, 0x56, 0x66, 0x30, 0xb1,
0x1f, 0x37, 0xf5, 0x4a, 0x79, 0x9c, 0x1c, 0x3d, 0x70, 0x1a, 0xbf, 0xa9, 0x57, 0xda, 0x86, 0x2a, 0x9f, 0x9d, 0x45, 0x66, 0x1f, 0x26, 0x57, 0x14, 0x1f, 0xc1, 0xf0, 0xb4, 0xc8, 0x97, 0x7c, 0x15,
0x8b, 0xf8, 0x33, 0x80, 0x8d, 0x0c, 0x5f, 0x89, 0x4d, 0x36, 0x5e, 0xf0, 0x1b, 0x2c, 0x99, 0xca, 0x1e, 0xc2, 0xe0, 0xb8, 0x96, 0x6b, 0xe5, 0x71, 0x72, 0xf4, 0xc0, 0x69, 0xfc, 0x5a, 0xae, 0xb5,
0x0c, 0xdf, 0x5b, 0x34, 0xf1, 0xe7, 0x10, 0x9e, 0x34, 0x22, 0x4b, 0xcf, 0xf3, 0x45, 0x81, 0xd4, 0x0d, 0x51, 0x16, 0xf1, 0x67, 0x00, 0x5b, 0x19, 0xbe, 0x2e, 0xdb, 0x6c, 0xbc, 0x60, 0x37, 0x58,
0xf1, 0x9a, 0xcb, 0x6a, 0x93, 0x2f, 0x0b, 0x31, 0xdc, 0xc8, 0x22, 0x5d, 0x0f, 0x19, 0x34, 0x1f, 0x32, 0x95, 0x19, 0xda, 0x77, 0x68, 0xe2, 0xcf, 0x21, 0x38, 0xa9, 0x79, 0x96, 0x9e, 0xe7, 0xcb,
0xa9, 0x3f, 0x27, 0x3e, 0xf9, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x77, 0xc4, 0xaa, 0x16, 0xae, 0x02, 0xa9, 0xe3, 0x35, 0x13, 0xd5, 0x36, 0x5f, 0x16, 0x62, 0xb8, 0x91, 0x45, 0xda, 0x1e, 0x32,
0x10, 0x00, 0x00, 0x68, 0x31, 0x54, 0x7f, 0x6a, 0x7c, 0xf2, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd3, 0xfb, 0xbd,
0x7b, 0xe6, 0x10, 0x00, 0x00,
} }

View File

@ -121,6 +121,8 @@ message Server {
bool Active = 7; // is this the currently active server for the source bool Active = 7; // is this the currently active server for the source
string Organization = 8; // Organization is the organization ID that resource belongs to string Organization = 8; // Organization is the organization ID that resource belongs to
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client
string Type = 10; // Type is the kind of the server (e.g. ifql)
string MetadataJSON = 11; // JSON byte representation of the metadata
} }
message Layout { message Layout {

View File

@ -368,6 +368,8 @@ type Server struct {
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted. InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool `json:"active"` // Is this the active server for the source? Active bool `json:"active"` // Is this the active server for the source?
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or ifql)
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
} }
// ServersStore stores connection information for a `Server` // ServersStore stores connection information for a `Server`

View File

@ -108,6 +108,7 @@ func TestServer(t *testing.T) {
"links": { "links": {
"self": "/chronograf/v1/sources/5000", "self": "/chronograf/v1/sources/5000",
"kapacitors": "/chronograf/v1/sources/5000/kapacitors", "kapacitors": "/chronograf/v1/sources/5000/kapacitors",
"services": "/chronograf/v1/sources/5000/services",
"proxy": "/chronograf/v1/sources/5000/proxy", "proxy": "/chronograf/v1/sources/5000/proxy",
"queries": "/chronograf/v1/sources/5000/queries", "queries": "/chronograf/v1/sources/5000/queries",
"write": "/chronograf/v1/sources/5000/write", "write": "/chronograf/v1/sources/5000/write",
@ -296,6 +297,7 @@ func TestServer(t *testing.T) {
"links": { "links": {
"self": "/chronograf/v1/sources/5000", "self": "/chronograf/v1/sources/5000",
"kapacitors": "/chronograf/v1/sources/5000/kapacitors", "kapacitors": "/chronograf/v1/sources/5000/kapacitors",
"services": "/chronograf/v1/sources/5000/services",
"proxy": "/chronograf/v1/sources/5000/proxy", "proxy": "/chronograf/v1/sources/5000/proxy",
"queries": "/chronograf/v1/sources/5000/queries", "queries": "/chronograf/v1/sources/5000/queries",
"write": "/chronograf/v1/sources/5000/write", "write": "/chronograf/v1/sources/5000/write",
@ -2943,7 +2945,7 @@ func TestServer(t *testing.T) {
serverURL := fmt.Sprintf("http://%v:%v%v", host, port, tt.args.path) serverURL := fmt.Sprintf("http://%v:%v%v", host, port, tt.args.path)
// Wait for the server to come online // Wait for the server to come online
timeout := time.Now().Add(5 * time.Second) timeout := time.Now().Add(30 * time.Second)
for { for {
_, err := http.Get(serverURL + "/swagger.json") _, err := http.Get(serverURL + "/swagger.json")
if err == nil { if err == nil {

View File

@ -154,7 +154,7 @@ func (s *Service) Kapacitors(w http.ResponseWriter, r *http.Request) {
srvs := []kapacitor{} srvs := []kapacitor{}
for _, srv := range mrSrvs { for _, srv := range mrSrvs {
if srv.SrcID == srcID { if srv.SrcID == srcID && srv.Type == "" {
srvs = append(srvs, newKapacitor(srv)) srvs = append(srvs, newKapacitor(srv))
} }
} }
@ -182,7 +182,7 @@ func (s *Service) KapacitorsID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id) srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID { if err != nil || srv.SrcID != srcID || srv.Type != "" {
notFound(w, id, s.Logger) notFound(w, id, s.Logger)
return return
} }
@ -207,7 +207,7 @@ func (s *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id) srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID { if err != nil || srv.SrcID != srcID || srv.Type != "" {
notFound(w, id, s.Logger) notFound(w, id, s.Logger)
return return
} }
@ -258,7 +258,7 @@ func (s *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id) srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID { if err != nil || srv.SrcID != srcID || srv.Type != "" {
notFound(w, id, s.Logger) notFound(w, id, s.Logger)
return return
} }

View File

@ -203,6 +203,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.RemoveSourceRole)) router.DELETE("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.RemoveSourceRole))
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.UpdateSourceRole)) router.PATCH("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.UpdateSourceRole))
// Services are resources that chronograf proxies to
router.GET("/chronograf/v1/sources/:id/services", EnsureViewer(service.Services))
router.POST("/chronograf/v1/sources/:id/services", EnsureEditor(service.NewService))
router.GET("/chronograf/v1/sources/:id/services/:kid", EnsureViewer(service.ServiceID))
router.PATCH("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.UpdateService))
router.DELETE("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.RemoveService))
// Service Proxy
router.GET("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureViewer(service.ProxyGet))
router.POST("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPost))
router.PATCH("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPatch))
router.DELETE("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyDelete))
// Kapacitor // Kapacitor
router.GET("/chronograf/v1/sources/:id/kapacitors", EnsureViewer(service.Kapacitors)) router.GET("/chronograf/v1/sources/:id/kapacitors", EnsureViewer(service.Kapacitors))
router.POST("/chronograf/v1/sources/:id/kapacitors", EnsureEditor(service.NewKapacitor)) router.POST("/chronograf/v1/sources/:id/kapacitors", EnsureEditor(service.NewKapacitor))
@ -221,10 +234,10 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesDelete)) router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesDelete))
// Kapacitor Proxy // Kapacitor Proxy
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureViewer(service.KapacitorProxyGet)) router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureViewer(service.ProxyGet))
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPost)) router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.ProxyPost))
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPatch)) router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.ProxyPatch))
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyDelete)) router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.ProxyDelete))
// Layouts // Layouts
router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts)) router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts))

View File

@ -11,8 +11,8 @@ import (
"time" "time"
) )
// KapacitorProxy proxies requests to kapacitor using the path query parameter. // Proxy proxies requests to services using the path query parameter.
func (s *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) { func (s *Service) Proxy(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r) srcID, err := paramID("id", r)
if err != nil { if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
@ -88,24 +88,24 @@ func (s *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
} }
// KapacitorProxyPost proxies POST to kapacitor // ProxyPost proxies POST to service
func (s *Service) KapacitorProxyPost(w http.ResponseWriter, r *http.Request) { func (s *Service) ProxyPost(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r) s.Proxy(w, r)
} }
// KapacitorProxyPatch proxies PATCH to kapacitor // ProxyPatch proxies PATCH to Service
func (s *Service) KapacitorProxyPatch(w http.ResponseWriter, r *http.Request) { func (s *Service) ProxyPatch(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r) s.Proxy(w, r)
} }
// KapacitorProxyGet proxies GET to kapacitor // ProxyGet proxies GET to service
func (s *Service) KapacitorProxyGet(w http.ResponseWriter, r *http.Request) { func (s *Service) ProxyGet(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r) s.Proxy(w, r)
} }
// KapacitorProxyDelete proxies DELETE to kapacitor // ProxyDelete proxies DELETE to service
func (s *Service) KapacitorProxyDelete(w http.ResponseWriter, r *http.Request) { func (s *Service) ProxyDelete(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r) s.Proxy(w, r)
} }
func singleJoiningSlash(a, b string) string { func singleJoiningSlash(a, b string) string {

320
server/services.go Normal file
View File

@ -0,0 +1,320 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/influxdata/chronograf"
)
type postServiceRequest struct {
Name *string `json:"name"` // User facing name of service instance.; Required: true
URL *string `json:"url"` // URL for the service backend (e.g. http://localhost:9092);/ Required: true
Type *string `json:"type"` // Type is the kind of service (e.g. ifql); Required
Username string `json:"username,omitempty"` // Username for authentication to service
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
}
func (p *postServiceRequest) Valid(defaultOrgID string) error {
if p.Name == nil || p.URL == nil {
return fmt.Errorf("name and url required")
}
if p.Type == nil {
return fmt.Errorf("type required")
}
if p.Organization == "" {
p.Organization = defaultOrgID
}
url, err := url.ParseRequestURI(*p.URL)
if err != nil {
return fmt.Errorf("invalid source URI: %v", err)
}
if len(url.Scheme) == 0 {
return fmt.Errorf("Invalid URL; no URL scheme defined")
}
return nil
}
type serviceLinks struct {
Proxy string `json:"proxy"` // URL location of proxy endpoint for this source
Self string `json:"self"` // Self link mapping to this resource
Source string `json:"source"` // URL location of the parent source
}
type service struct {
ID int `json:"id,string"` // Unique identifier representing a service instance.
SrcID int `json:"sourceID,string"` // SrcID of the data source
Name string `json:"name"` // User facing name of service instance.
URL string `json:"url"` // URL for the service backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to service
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
Type string `json:"type"` // Type is the kind of service (e.g. ifql)
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
Links serviceLinks `json:"links"` // Links are URI locations related to service
}
func newService(srv chronograf.Server) service {
if srv.Metadata == nil {
srv.Metadata = make(map[string]interface{})
}
httpAPISrcs := "/chronograf/v1/sources"
return service{
ID: srv.ID,
SrcID: srv.SrcID,
Name: srv.Name,
Username: srv.Username,
URL: srv.URL,
InsecureSkipVerify: srv.InsecureSkipVerify,
Type: srv.Type,
Metadata: srv.Metadata,
Links: serviceLinks{
Self: fmt.Sprintf("%s/%d/services/%d", httpAPISrcs, srv.SrcID, srv.ID),
Source: fmt.Sprintf("%s/%d", httpAPISrcs, srv.SrcID),
Proxy: fmt.Sprintf("%s/%d/services/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
},
}
}
type services struct {
Services []service `json:"services"`
}
// NewService adds valid service store store.
func (s *Service) NewService(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
_, err = s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, s.Logger)
return
}
var req postServiceRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := req.Valid(defaultOrg.ID); err != nil {
invalidData(w, err, s.Logger)
return
}
srv := chronograf.Server{
SrcID: srcID,
Name: *req.Name,
Username: req.Username,
Password: req.Password,
InsecureSkipVerify: req.InsecureSkipVerify,
URL: *req.URL,
Organization: req.Organization,
Type: *req.Type,
Metadata: req.Metadata,
}
if srv, err = s.Store.Servers(ctx).Add(ctx, srv); err != nil {
msg := fmt.Errorf("Error storing service %v: %v", req, err)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
res := newService(srv)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
// Services retrieves all services from store.
func (s *Service) Services(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
mrSrvs, err := s.Store.Servers(ctx).All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading services", s.Logger)
return
}
srvs := []service{}
for _, srv := range mrSrvs {
if srv.SrcID == srcID && srv.Type != "" {
srvs = append(srvs, newService(srv))
}
}
res := services{
Services: srvs,
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// ServiceID retrieves a service with ID from store.
func (s *Service) ServiceID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID || srv.Type == "" {
notFound(w, id, s.Logger)
return
}
res := newService(srv)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveService deletes service from store.
func (s *Service) RemoveService(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID || srv.Type == "" {
notFound(w, id, s.Logger)
return
}
if err = s.Store.Servers(ctx).Delete(ctx, srv); err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
type patchServiceRequest struct {
Name *string `json:"name,omitempty"` // User facing name of service instance.
Type *string `json:"type,omitempty"` // Type is the kind of service (e.g. ifql)
URL *string `json:"url,omitempty"` // URL for the service
Username *string `json:"username,omitempty"` // Username for service auth
Password *string `json:"password,omitempty"`
InsecureSkipVerify *bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
Metadata *map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
}
func (p *patchServiceRequest) Valid() error {
if p.URL != nil {
url, err := url.ParseRequestURI(*p.URL)
if err != nil {
return fmt.Errorf("invalid source URI: %v", err)
}
if len(url.Scheme) == 0 {
return fmt.Errorf("Invalid URL; no URL scheme defined")
}
}
if p.Type != nil && *p.Type == "" {
return fmt.Errorf("Invalid type; type must not be an empty string")
}
return nil
}
// UpdateService incrementally updates a service definition in the store
func (s *Service) UpdateService(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID || srv.Type == "" {
notFound(w, id, s.Logger)
return
}
var req patchServiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, s.Logger)
return
}
if req.Name != nil {
srv.Name = *req.Name
}
if req.Type != nil {
srv.Type = *req.Type
}
if req.URL != nil {
srv.URL = *req.URL
}
if req.Password != nil {
srv.Password = *req.Password
}
if req.Username != nil {
srv.Username = *req.Username
}
if req.InsecureSkipVerify != nil {
srv.InsecureSkipVerify = *req.InsecureSkipVerify
}
if req.Metadata != nil {
srv.Metadata = *req.Metadata
}
if err := s.Store.Servers(ctx).Update(ctx, srv); err != nil {
msg := fmt.Sprintf("Error updating service ID %d", id)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
res := newService(srv)
encodeJSON(w, http.StatusOK, res, s.Logger)
}

View File

@ -17,6 +17,7 @@ import (
type sourceLinks struct { type sourceLinks struct {
Self string `json:"self"` // Self link mapping to this resource Self string `json:"self"` // Self link mapping to this resource
Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint
Services string `json:"services"` // URL for services endpoint
Proxy string `json:"proxy"` // URL for proxy endpoint Proxy string `json:"proxy"` // URL for proxy endpoint
Queries string `json:"queries"` // URL for the queries analysis endpoint Queries string `json:"queries"` // URL for the queries analysis endpoint
Write string `json:"write"` // URL for the write line-protocol endpoint Write string `json:"write"` // URL for the write line-protocol endpoint
@ -49,6 +50,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
Links: sourceLinks{ Links: sourceLinks{
Self: fmt.Sprintf("%s/%d", httpAPISrcs, src.ID), Self: fmt.Sprintf("%s/%d", httpAPISrcs, src.ID),
Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID), Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID),
Services: fmt.Sprintf("%s/%d/services", httpAPISrcs, src.ID),
Proxy: fmt.Sprintf("%s/%d/proxy", httpAPISrcs, src.ID), Proxy: fmt.Sprintf("%s/%d/proxy", httpAPISrcs, src.ID),
Queries: fmt.Sprintf("%s/%d/queries", httpAPISrcs, src.ID), Queries: fmt.Sprintf("%s/%d/queries", httpAPISrcs, src.ID),
Write: fmt.Sprintf("%s/%d/write", httpAPISrcs, src.ID), Write: fmt.Sprintf("%s/%d/write", httpAPISrcs, src.ID),

View File

@ -175,6 +175,7 @@ func Test_newSourceResponse(t *testing.T) {
}, },
Links: sourceLinks{ Links: sourceLinks{
Self: "/chronograf/v1/sources/1", Self: "/chronograf/v1/sources/1",
Services: "/chronograf/v1/sources/1/services",
Proxy: "/chronograf/v1/sources/1/proxy", Proxy: "/chronograf/v1/sources/1/proxy",
Queries: "/chronograf/v1/sources/1/queries", Queries: "/chronograf/v1/sources/1/queries",
Write: "/chronograf/v1/sources/1/write", Write: "/chronograf/v1/sources/1/write",
@ -201,6 +202,7 @@ func Test_newSourceResponse(t *testing.T) {
Links: sourceLinks{ Links: sourceLinks{
Self: "/chronograf/v1/sources/1", Self: "/chronograf/v1/sources/1",
Proxy: "/chronograf/v1/sources/1/proxy", Proxy: "/chronograf/v1/sources/1/proxy",
Services: "/chronograf/v1/sources/1/services",
Queries: "/chronograf/v1/sources/1/queries", Queries: "/chronograf/v1/sources/1/queries",
Write: "/chronograf/v1/sources/1/write", Write: "/chronograf/v1/sources/1/write",
Kapacitors: "/chronograf/v1/sources/1/kapacitors", Kapacitors: "/chronograf/v1/sources/1/kapacitors",
@ -440,7 +442,7 @@ func TestService_SourcesID(t *testing.T) {
ID: "1", ID: "1",
wantStatusCode: 200, wantStatusCode: 200,
wantContentType: "application/json", wantContentType: "application/json",
wantBody: `{"id":"1","name":"","url":"","default":false,"telegraf":"telegraf","organization":"","defaultRP":"","links":{"self":"/chronograf/v1/sources/1","kapacitors":"/chronograf/v1/sources/1/kapacitors","proxy":"/chronograf/v1/sources/1/proxy","queries":"/chronograf/v1/sources/1/queries","write":"/chronograf/v1/sources/1/write","permissions":"/chronograf/v1/sources/1/permissions","users":"/chronograf/v1/sources/1/users","databases":"/chronograf/v1/sources/1/dbs","annotations":"/chronograf/v1/sources/1/annotations","health":"/chronograf/v1/sources/1/health"}} wantBody: `{"id":"1","name":"","url":"","default":false,"telegraf":"telegraf","organization":"","defaultRP":"","links":{"self":"/chronograf/v1/sources/1","kapacitors":"/chronograf/v1/sources/1/kapacitors","services":"/chronograf/v1/sources/1/services","proxy":"/chronograf/v1/sources/1/proxy","queries":"/chronograf/v1/sources/1/queries","write":"/chronograf/v1/sources/1/write","permissions":"/chronograf/v1/sources/1/permissions","users":"/chronograf/v1/sources/1/users","databases":"/chronograf/v1/sources/1/dbs","annotations":"/chronograf/v1/sources/1/annotations","health":"/chronograf/v1/sources/1/health"}}
`, `,
}, },
} }
@ -530,7 +532,7 @@ func TestService_UpdateSource(t *testing.T) {
wantStatusCode: 200, wantStatusCode: 200,
wantContentType: "application/json", wantContentType: "application/json",
wantBody: func(url string) string { wantBody: func(url string) string {
return fmt.Sprintf(`{"id":"1","name":"marty","type":"influx","username":"bob","url":"%s","metaUrl":"http://murl","default":false,"telegraf":"murlin","organization":"1337","defaultRP":"pineapple","links":{"self":"/chronograf/v1/sources/1","kapacitors":"/chronograf/v1/sources/1/kapacitors","proxy":"/chronograf/v1/sources/1/proxy","queries":"/chronograf/v1/sources/1/queries","write":"/chronograf/v1/sources/1/write","permissions":"/chronograf/v1/sources/1/permissions","users":"/chronograf/v1/sources/1/users","databases":"/chronograf/v1/sources/1/dbs","annotations":"/chronograf/v1/sources/1/annotations","health":"/chronograf/v1/sources/1/health"}} return fmt.Sprintf(`{"id":"1","name":"marty","type":"influx","username":"bob","url":"%s","metaUrl":"http://murl","default":false,"telegraf":"murlin","organization":"1337","defaultRP":"pineapple","links":{"self":"/chronograf/v1/sources/1","kapacitors":"/chronograf/v1/sources/1/kapacitors","services":"/chronograf/v1/sources/1/services","proxy":"/chronograf/v1/sources/1/proxy","queries":"/chronograf/v1/sources/1/queries","write":"/chronograf/v1/sources/1/write","permissions":"/chronograf/v1/sources/1/permissions","users":"/chronograf/v1/sources/1/users","databases":"/chronograf/v1/sources/1/dbs","annotations":"/chronograf/v1/sources/1/annotations","health":"/chronograf/v1/sources/1/health"}}
`, url) `, url)
}, },
}, },

View File

@ -5,3 +5,4 @@ export const getAST = jest.fn(() => Promise.resolve({}))
export const getDatabases = jest.fn(() => export const getDatabases = jest.fn(() =>
Promise.resolve(['db1', 'db2', 'db3']) Promise.resolve(['db1', 'db2', 'db3'])
) )
export const getTimeSeries = jest.fn(() => Promise.resolve({data: ''}))

View File

@ -9,7 +9,7 @@
"url": "github:influxdata/chronograf" "url": "github:influxdata/chronograf"
}, },
"scripts": { "scripts": {
"build": "yarn run clean && webpack --config ./webpack/prod.config.js", "build": "yarn run clean && webpack --config ./webpack/prod.config.js --display-error-details",
"build:dev": "webpack --config ./webpack/dev.config.js", "build:dev": "webpack --config ./webpack/dev.config.js",
"build:vendor": "webpack --config webpack/vendor.config.js", "build:vendor": "webpack --config webpack/vendor.config.js",
"start": "yarn run clean && yarn run build:vendor && webpack --watch --config ./webpack/dev.config.js --progress", "start": "yarn run clean && yarn run build:vendor && webpack --watch --config ./webpack/dev.config.js --progress",

View File

@ -34,6 +34,20 @@ export const getAST = async (request: ASTRequest) => {
} }
} }
export const getTimeSeries = async (script: string) => {
try {
const data = await AJAX({
method: 'POST',
url: `http://localhost:8093/query?q=${script}`,
})
return data
} catch (error) {
console.error('Problem fetching data', error)
throw error
}
}
// TODO: replace with actual requests to IFQL daemon // TODO: replace with actual requests to IFQL daemon
export const getDatabases = async () => { export const getDatabases = async () => {
try { try {

View File

@ -218,10 +218,9 @@ export default class Walker {
return [...this.walk(currentNode.argument), {name, args, source}] return [...this.walk(currentNode.argument), {name, args, source}]
} }
if (currentNode.type === 'ArrowFunctionExpression') { if (currentNode.type === 'Identifier') {
const params = currentNode.params name = currentNode.name
const body = currentNode.body return [{name, source}]
return [{name, params, body}]
} }
name = currentNode.callee.name name = currentNode.callee.name

View File

@ -4,12 +4,15 @@ import _ from 'lodash'
import ExpressionNode from 'src/ifql/components/ExpressionNode' import ExpressionNode from 'src/ifql/components/ExpressionNode'
import VariableName from 'src/ifql/components/VariableName' import VariableName from 'src/ifql/components/VariableName'
import FuncSelector from 'src/ifql/components/FuncSelector' import FuncSelector from 'src/ifql/components/FuncSelector'
import {funcNames} from 'src/ifql/constants'
import {FlatBody, Suggestion} from 'src/types/ifql' import {FlatBody, Suggestion} from 'src/types/ifql'
interface Props { interface Props {
body: Body[] body: Body[]
suggestions: Suggestion[] suggestions: Suggestion[]
onAppendFrom: () => void
onAppendJoin: () => void
} }
interface Body extends FlatBody { interface Body extends FlatBody {
@ -18,15 +21,14 @@ interface Body extends FlatBody {
class BodyBuilder extends PureComponent<Props> { class BodyBuilder extends PureComponent<Props> {
public render() { public render() {
const bodybuilder = this.props.body.map(b => { const bodybuilder = this.props.body.map((b, i) => {
if (b.declarations.length) { if (b.declarations.length) {
return b.declarations.map(d => { return b.declarations.map(d => {
if (d.funcs) { if (d.funcs) {
return ( return (
<div className="declaration" key={b.id}> <div className="declaration" key={i}>
<VariableName name={d.name} /> <VariableName name={d.name} />
<ExpressionNode <ExpressionNode
key={b.id}
bodyID={b.id} bodyID={b.id}
declarationID={d.id} declarationID={d.id}
funcNames={this.funcNames} funcNames={this.funcNames}
@ -37,7 +39,7 @@ class BodyBuilder extends PureComponent<Props> {
} }
return ( return (
<div className="declaration" key={b.id}> <div className="declaration" key={i}>
<VariableName name={b.source} /> <VariableName name={b.source} />
</div> </div>
) )
@ -45,8 +47,7 @@ class BodyBuilder extends PureComponent<Props> {
} }
return ( return (
<div className="declaration" key={b.id}> <div className="declaration" key={i}>
<VariableName />
<ExpressionNode <ExpressionNode
bodyID={b.id} bodyID={b.id}
funcs={b.funcs} funcs={b.funcs}
@ -63,7 +64,7 @@ class BodyBuilder extends PureComponent<Props> {
<FuncSelector <FuncSelector
bodyID="fake-body-id" bodyID="fake-body-id"
declarationID="fake-declaration-id" declarationID="fake-declaration-id"
onAddNode={this.createNewDeclaration} onAddNode={this.createNewBody}
funcs={this.newDeclarationFuncs} funcs={this.newDeclarationFuncs}
connectorVisible={false} connectorVisible={false}
/> />
@ -73,15 +74,21 @@ class BodyBuilder extends PureComponent<Props> {
} }
private get newDeclarationFuncs(): string[] { private get newDeclarationFuncs(): string[] {
// 'JOIN' only available if there are at least 2 named declarations const {body} = this.props
return ['from', 'join', 'variable'] const declarationFunctions = [funcNames.FROM]
if (body.length > 1) {
declarationFunctions.push(funcNames.JOIN)
}
return declarationFunctions
} }
private createNewDeclaration = (bodyID, name, declarationID) => { private createNewBody = name => {
// Returning a string here so linter stops yelling if (name === funcNames.FROM) {
// TODO: write a real function this.props.onAppendFrom()
}
return `${bodyID} / ${name} / ${declarationID}` if (name === funcNames.JOIN) {
this.props.onAppendJoin()
}
} }
private get funcNames() { private get funcNames() {

View File

@ -22,9 +22,9 @@ class ExpressionNode extends PureComponent<Props> {
{({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => { {({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => {
return ( return (
<> <>
{funcs.map(func => ( {funcs.map((func, i) => (
<FuncNode <FuncNode
key={func.id} key={i}
func={func} func={func}
bodyID={bodyID} bodyID={bodyID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}

View File

@ -0,0 +1,65 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import IFQLForm from 'src/ifql/components/IFQLForm'
import {Service, Notification} from 'src/types'
import {ifqlUpdated, ifqlNotUpdated} from 'src/shared/copy/notifications'
import {UpdateServiceAsync} from 'src/shared/actions/services'
interface Props {
service: Service
onDismiss: () => void
updateService: UpdateServiceAsync
notify: (message: Notification) => void
}
interface State {
service: Service
}
class IFQLEdit extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
service: this.props.service,
}
}
public render() {
return (
<IFQLForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode="edit"
/>
)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
const {value, name} = e.target
const update = {[name]: value}
this.setState({service: {...this.state.service, ...update}})
}
private handleSubmit = async (
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {notify, onDismiss, updateService} = this.props
const {service} = this.state
try {
await updateService(service)
} catch (error) {
notify(ifqlNotUpdated(error.message))
return
}
notify(ifqlUpdated)
onDismiss()
}
}
export default IFQLEdit

View File

@ -0,0 +1,72 @@
import React, {ChangeEvent, PureComponent} from 'react'
import Input from 'src/kapacitor/components/KapacitorFormInput'
import {NewService} from 'src/types'
interface Props {
service: NewService
mode: string
onSubmit: (e: ChangeEvent<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
}
class IFQLForm extends PureComponent<Props> {
public render() {
const {service, onSubmit, onInputChange} = this.props
return (
<div className="template-variable-manager--body">
<form onSubmit={onSubmit} style={{display: 'inline-block'}}>
<Input
name="url"
label="IFQL URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
/>
<Input
name="name"
label="Name"
value={service.name}
placeholder={service.name}
onChange={onInputChange}
maxLength={33}
/>
<div className="form-group form-group-submit col-xs-12 text-center">
<button
className="btn btn-success"
type="submit"
data-test="submit-button"
>
{this.buttonText}
</button>
</div>
</form>
</div>
)
}
private get buttonText(): string {
const {mode} = this.props
if (mode === 'edit') {
return 'Update'
}
return 'Connect'
}
private get url(): string {
const {
service: {url},
} = this.props
if (url) {
return url
}
return ''
}
}
export default IFQLForm

View File

@ -0,0 +1,67 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {
showOverlay as showOverlayAction,
ShowOverlay,
} from 'src/shared/actions/overlayTechnology'
import {Service} from 'src/types'
interface Props {
showOverlay: ShowOverlay
service: Service
onGetTimeSeries: () => void
}
class IFQLHeader extends PureComponent<Props> {
public render() {
const {onGetTimeSeries} = this.props
return (
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Time Machine</h1>
</div>
<div className="page-header__right">
<button onClick={this.overlay} className="btn btn-sm btn-default">
Edit Connection
</button>
<button
className="btn btn-sm btn-primary"
onClick={onGetTimeSeries}
>
Get Data!
</button>
</div>
</div>
</div>
)
}
private overlay = () => {
const {showOverlay, service} = this.props
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<IFQLOverlay
mode="edit"
service={service}
onDismiss={onDismissOverlay}
/>
)}
</OverlayContext.Consumer>,
{}
)
}
}
const mdtp = {
showOverlay: showOverlayAction,
}
export default connect(null, mdtp)(IFQLHeader)

View File

@ -0,0 +1,86 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import IFQLForm from 'src/ifql/components/IFQLForm'
import {NewService, Source, Notification} from 'src/types'
import {ifqlCreated, ifqlNotCreated} from 'src/shared/copy/notifications'
import {CreateServiceAsync} from 'src/shared/actions/services'
interface Props {
source: Source
onDismiss: () => void
createService: CreateServiceAsync
notify: (message: Notification) => void
}
interface State {
service: NewService
}
const port = 8093
class IFQLNew extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
service: this.defaultService,
}
}
public render() {
return (
<IFQLForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode="new"
/>
)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
const {value, name} = e.target
const update = {[name]: value}
this.setState({service: {...this.state.service, ...update}})
}
private handleSubmit = async (
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {notify, source, onDismiss, createService} = this.props
const {service} = this.state
try {
await createService(source, service)
} catch (error) {
notify(ifqlNotCreated(error.message))
return
}
notify(ifqlCreated)
onDismiss()
}
private get defaultService(): NewService {
return {
name: 'IFQL',
url: this.url,
username: '',
insecureSkipVerify: false,
type: 'ifql',
active: true,
}
}
private get url(): string {
const parser = document.createElement('a')
parser.href = this.props.source.url
return `${parser.protocol}//${parser.hostname}:${port}`
}
}
export default IFQLNew

View File

@ -0,0 +1,86 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import IFQLNew from 'src/ifql/components/IFQLNew'
import IFQLEdit from 'src/ifql/components/IFQLEdit'
import {Service, Source, Notification} from 'src/types'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
updateServiceAsync,
UpdateServiceAsync,
createServiceAsync,
CreateServiceAsync,
} from 'src/shared/actions/services'
interface Props {
mode: string
source?: Source
service?: Service
onDismiss: () => void
notify: (message: Notification) => void
createService: CreateServiceAsync
updateService: UpdateServiceAsync
}
class IFQLOverlay extends PureComponent<Props> {
public render() {
return (
<div className="ifql-overlay">
<div className="template-variable-manager--header">
<div className="page-header__left">
<h1 className="page-header__title">Connect to IFQL</h1>
</div>
<div className="page-header__right">
<span
className="page-header__dismiss"
onClick={this.props.onDismiss}
/>
</div>
</div>
{this.form}
</div>
)
}
private get form(): JSX.Element {
const {
mode,
source,
service,
notify,
onDismiss,
createService,
updateService,
} = this.props
if (mode === 'new') {
return (
<IFQLNew
source={source}
notify={notify}
onDismiss={onDismiss}
createService={createService}
/>
)
}
return (
<IFQLEdit
notify={notify}
service={service}
onDismiss={onDismiss}
updateService={updateService}
/>
)
}
}
const mdtp = {
notify: notifyAction,
createService: createServiceAsync,
updateService: updateServiceAsync,
}
export default connect(null, mdtp)(IFQLOverlay)

View File

@ -3,16 +3,28 @@ import SchemaExplorer from 'src/ifql/components/SchemaExplorer'
import BodyBuilder from 'src/ifql/components/BodyBuilder' import BodyBuilder from 'src/ifql/components/BodyBuilder'
import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor'
import TimeMachineVis from 'src/ifql/components/TimeMachineVis' import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
import Threesizer from 'src/shared/components/Threesizer' import Threesizer from 'src/shared/components/threesizer/Threesizer'
import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql' import {
Suggestion,
OnChangeScript,
OnSubmitScript,
FlatBody,
Status,
} from 'src/types/ifql'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants' import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props { interface Props {
data: string
script: string script: string
suggestions: Suggestion[]
body: Body[] body: Body[]
status: Status
suggestions: Suggestion[]
onChangeScript: OnChangeScript onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript
onAppendFrom: () => void
onAppendJoin: () => void
onAnalyze: () => void
} }
interface Body extends FlatBody { interface Body extends FlatBody {
@ -32,9 +44,12 @@ class TimeMachine extends PureComponent<Props> {
} }
private get mainSplit() { private get mainSplit() {
const {data} = this.props
return [ return [
{ {
handleDisplay: 'none', handleDisplay: 'none',
menuOptions: [],
headerButtons: [],
render: () => ( render: () => (
<Threesizer <Threesizer
divisions={this.divisions} divisions={this.divisions}
@ -44,31 +59,67 @@ class TimeMachine extends PureComponent<Props> {
}, },
{ {
handlePixels: 8, handlePixels: 8,
render: () => <TimeMachineVis blob="Visualizer" />, menuOptions: [],
headerButtons: [],
render: () => <TimeMachineVis data={data} />,
}, },
] ]
} }
private get divisions() { private get divisions() {
const {body, suggestions, script, onChangeScript} = this.props const {
body,
script,
status,
onAnalyze,
suggestions,
onAppendFrom,
onAppendJoin,
onChangeScript,
onSubmitScript,
} = this.props
return [ return [
{ {
name: 'Explore', name: 'Explore',
headerButtons: [],
menuOptions: [],
render: () => <SchemaExplorer />, render: () => <SchemaExplorer />,
}, },
{ {
name: 'Script', name: 'Script',
headerButtons: [
<div
key="analyze"
className="btn btn-default btn-sm analyze--button"
onClick={onAnalyze}
>
Analyze
</div>,
],
menuOptions: [],
render: visibility => ( render: visibility => (
<TimeMachineEditor <TimeMachineEditor
status={status}
script={script} script={script}
onChangeScript={onChangeScript}
visibility={visibility} visibility={visibility}
onChangeScript={onChangeScript}
onSubmitScript={onSubmitScript}
/> />
), ),
}, },
{ {
name: 'Build', name: 'Build',
render: () => <BodyBuilder body={body} suggestions={suggestions} />, headerButtons: [],
menuOptions: [],
render: () => (
<BodyBuilder
body={body}
suggestions={suggestions}
onAppendFrom={onAppendFrom}
onAppendJoin={onAppendJoin}
/>
),
}, },
] ]
} }

View File

@ -3,13 +3,25 @@ import {Controlled as CodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange} from 'codemirror' import {EditorChange} from 'codemirror'
import 'src/external/codemirror' import 'src/external/codemirror'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeScript} from 'src/types/ifql' import {OnChangeScript, OnSubmitScript} from 'src/types/ifql'
import {editor} from 'src/ifql/constants' import {editor} from 'src/ifql/constants'
interface Gutter {
line: number
text: string
}
interface Status {
type: string
text: string
}
interface Props { interface Props {
script: string script: string
onChangeScript: OnChangeScript
visibility: string visibility: string
status: Status
onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript
} }
interface EditorInstance extends IInstance { interface EditorInstance extends IInstance {
@ -19,12 +31,21 @@ interface EditorInstance extends IInstance {
@ErrorHandling @ErrorHandling
class TimeMachineEditor extends PureComponent<Props> { class TimeMachineEditor extends PureComponent<Props> {
private editor: EditorInstance private editor: EditorInstance
private prevKey: string
constructor(props) { constructor(props) {
super(props) super(props)
} }
public componentDidUpdate(prevProps) { public componentDidUpdate(prevProps) {
if (this.props.status.type === 'error') {
this.makeError()
}
if (this.props.status.type !== 'error') {
this.editor.clearGutter('error-gutter')
}
if (prevProps.visibility === this.props.visibility) { if (prevProps.visibility === this.props.visibility) {
return return
} }
@ -45,6 +66,7 @@ class TimeMachineEditor extends PureComponent<Props> {
extraKeys: {'Ctrl-Space': 'autocomplete'}, extraKeys: {'Ctrl-Space': 'autocomplete'},
completeSingle: false, completeSingle: false,
autoRefresh: true, autoRefresh: true,
gutters: ['error-gutter'],
} }
return ( return (
@ -58,17 +80,71 @@ class TimeMachineEditor extends PureComponent<Props> {
onBeforeChange={this.updateCode} onBeforeChange={this.updateCode}
onTouchStart={this.onTouchStart} onTouchStart={this.onTouchStart}
editorDidMount={this.handleMount} editorDidMount={this.handleMount}
onBlur={this.handleBlur}
/> />
</div> </div>
) )
} }
private handleBlur = (): void => {
this.props.onSubmitScript()
}
private makeError(): void {
this.editor.clearGutter('error-gutter')
const lineNumbers = this.statusLine
lineNumbers.forEach(({line, text}) => {
this.editor.setGutterMarker(
line - 1,
'error-gutter',
this.errorMarker(text)
)
})
this.editor.refresh()
}
private errorMarker(message: string): HTMLElement {
const span = document.createElement('span')
span.className = 'icon stop error-warning'
span.title = message
return span
}
private get statusLine(): Gutter[] {
const {status} = this.props
const messages = status.text.split('\n')
const lineNumbers = messages.map(text => {
const [numbers] = text.split(' ')
const [lineNumber] = numbers.split(':')
return {line: Number(lineNumber), text}
})
return lineNumbers
}
private handleMount = (instance: EditorInstance) => { private handleMount = (instance: EditorInstance) => {
instance.refresh() // required to for proper line height on mount
this.editor = instance this.editor = instance
} }
private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => { private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => {
const {key} = e const {key} = e
const prevKey = this.prevKey
if (
prevKey === 'Control' ||
prevKey === 'Meta' ||
(prevKey === 'Shift' && key === '.')
) {
return (this.prevKey = key)
}
this.prevKey = key
if (editor.EXCLUDED_KEYS.includes(key)) {
return
}
if (editor.EXCLUDED_KEYS.includes(key)) { if (editor.EXCLUDED_KEYS.includes(key)) {
return return

View File

@ -1,14 +1,41 @@
import React, {SFC} from 'react' import React, {PureComponent} from 'react'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
blob: string data: string
} }
const TimeMachineVis: SFC<Props> = ({blob}) => (
@ErrorHandling
class TimeMachineVis extends PureComponent<Props> {
public render() {
return (
<div className="time-machine-visualization"> <div className="time-machine-visualization">
<div className="time-machine--graph"> <div className="time-machine--graph">
<div className="time-machine--graph-body">{blob}</div> <FancyScrollbar>
<div className="time-machine--graph-body">
{this.data.map((d, i) => {
return (
<div key={i} className="data-row">
{d}
</div>
)
})}
</div>
</FancyScrollbar>
</div> </div>
</div> </div>
) )
}
private get data(): string[] {
const {data} = this.props
if (!data) {
return ['Your query was syntactically correct but returned no data']
}
return this.props.data.split('\n')
}
}
export default TimeMachineVis export default TimeMachineVis

View File

@ -1,4 +1,4 @@
import React, {PureComponent, MouseEvent} from 'react' import React, {PureComponent} from 'react'
interface Props { interface Props {
name?: string name?: string
@ -22,78 +22,12 @@ export default class VariableName extends PureComponent<Props, State> {
} }
public render() { public render() {
const {isExpanded} = this.state return <div className="variable-string">{this.nameElement}</div>
return (
<div
className="variable-string"
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{this.nameElement}
{isExpanded && this.renderTooltip}
</div>
)
}
private get renderTooltip(): JSX.Element {
const {name} = this.props
if (name.includes('=')) {
const split = name.split('=')
const varName = split[0].substring(0, split[0].length - 1)
const varValue = split[1].substring(1)
return (
<div className="variable-name--tooltip">
<input
type="text"
className="form-control form-plutonium input-sm variable-name--input"
defaultValue={varName}
placeholder="Name"
/>
<span className="variable-name--operator">=</span>
<input
type="text"
className="form-control input-sm variable-name--input"
defaultValue={varValue}
placeholder="Value"
/>
</div>
)
}
return (
<div className="variable-name--tooltip">
<input
type="text"
className="form-control form-plutonium input-sm variable-name--input"
defaultValue={name}
placeholder="Name this query..."
/>
</div>
)
}
private handleMouseEnter = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
this.setState({isExpanded: true})
}
private handleMouseLeave = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
this.setState({isExpanded: false})
} }
private get nameElement(): JSX.Element { private get nameElement(): JSX.Element {
const {name} = this.props const {name} = this.props
if (!name) {
return <span className="variable-blank">Untitled</span>
}
if (name.includes('=')) { if (name.includes('=')) {
return this.colorizeSyntax return this.colorizeSyntax
} }
@ -105,7 +39,7 @@ export default class VariableName extends PureComponent<Props, State> {
const {name} = this.props const {name} = this.props
const split = name.split('=') const split = name.split('=')
const varName = split[0].substring(0, split[0].length - 1) const varName = split[0].substring(0, split[0].length - 1)
const varValue = split[1].substring(1) const varValue = this.props.name.replace(/^[^=]+=/, '')
const valueIsString = varValue.endsWith('"') const valueIsString = varValue.endsWith('"')

View File

@ -0,0 +1,2 @@
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
export const NEW_JOIN = `join(tables: {fil:fil, tele:tele}, on:["host"], fn: (tables) => tables.fil["_value"] + tables.tele["_value"])`

View File

@ -30,6 +30,9 @@ export const EXCLUDED_KEYS = [
'Subtract', 'Subtract',
'Decimal point', 'Decimal point',
'Divide', 'Divide',
'>',
'|',
')',
'F1', 'F1',
'F2', 'F2',
'F3', 'F3',

View File

@ -1,2 +1,3 @@
export const FROM = 'from' export const FROM = 'from'
export const FILTER = 'filter' export const FILTER = 'filter'
export const JOIN = 'join'

View File

@ -1,6 +1,7 @@
import * as funcNames from 'src/ifql/constants/funcNames'
import * as argTypes from 'src/ifql/constants/argumentTypes'
import {ast} from 'src/ifql/constants/ast' import {ast} from 'src/ifql/constants/ast'
import * as editor from 'src/ifql/constants/editor' import * as editor from 'src/ifql/constants/editor'
import * as argTypes from 'src/ifql/constants/argumentTypes'
import * as funcNames from 'src/ifql/constants/funcNames'
import * as builder from 'src/ifql/constants/builder'
export {ast, funcNames, argTypes, editor} export {ast, funcNames, argTypes, editor, builder}

View File

@ -0,0 +1,72 @@
import React, {PureComponent, ReactChildren} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Source, Service} from 'src/types'
import * as a from 'src/shared/actions/overlayTechnology'
import * as b from 'src/shared/actions/services'
const actions = {...a, ...b}
interface Props {
sources: Source[]
services: Service[]
children: ReactChildren
showOverlay: a.ShowOverlay
fetchServicesAsync: b.FetchServicesAsync
}
export class CheckServices extends PureComponent<Props & WithRouterProps> {
public async componentDidMount() {
const source = this.props.sources.find(
s => s.id === this.props.params.sourceID
)
if (!source) {
return
}
await this.props.fetchServicesAsync(source)
if (!this.props.services.length) {
this.overlay()
}
}
public render() {
return this.props.children
}
private overlay() {
const {showOverlay, services, sources, params} = this.props
const source = sources.find(s => s.id === params.sourceID)
if (services.length) {
return
}
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<IFQLOverlay
mode="new"
source={source}
onDismiss={onDismissOverlay}
/>
)}
</OverlayContext.Consumer>,
{}
)
}
}
const mdtp = {
fetchServicesAsync: actions.fetchServicesAsync,
showOverlay: actions.showOverlay,
}
const mstp = ({sources, services}) => ({sources, services})
export default connect(mstp, mdtp)(withRouter(CheckServices))

View File

@ -1,20 +1,42 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import _ from 'lodash' import _ from 'lodash'
import CheckServices from 'src/ifql/containers/CheckServices'
import TimeMachine from 'src/ifql/components/TimeMachine' import TimeMachine from 'src/ifql/components/TimeMachine'
import IFQLHeader from 'src/ifql/components/IFQLHeader'
import {ErrorHandling} from 'src/shared/decorators/errors'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {Suggestion, FlatBody, Links} from 'src/types/ifql'
import {InputArg, Handlers, DeleteFuncNodeArgs, Func} from 'src/types/ifql' import {notify as notifyAction} from 'src/shared/actions/notifications'
import {analyzeSuccess} from 'src/shared/copy/notifications'
import {bodyNodes} from 'src/ifql/helpers' import {bodyNodes} from 'src/ifql/helpers'
import {getSuggestions, getAST} from 'src/ifql/apis' import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
import * as argTypes from 'src/ifql/constants/argumentTypes' import {builder, argTypes} from 'src/ifql/constants'
import {ErrorHandling} from 'src/shared/decorators/errors' import {funcNames} from 'src/ifql/constants'
import {Source, Service, Notification} from 'src/types'
import {
Suggestion,
FlatBody,
Links,
InputArg,
Handlers,
DeleteFuncNodeArgs,
Func,
} from 'src/types/ifql'
interface Status {
type: string
text: string
}
interface Props { interface Props {
links: Links links: Links
services: Service[]
sources: Source[]
notify: (message: Notification) => void
} }
interface Body extends FlatBody { interface Body extends FlatBody {
@ -25,7 +47,9 @@ interface State {
body: Body[] body: Body[]
ast: object ast: object
script: string script: string
data: string
suggestions: Suggestion[] suggestions: Suggestion[]
status: Status
} }
export const IFQLContext = React.createContext() export const IFQLContext = React.createContext()
@ -37,11 +61,13 @@ export class IFQLPage extends PureComponent<Props, State> {
this.state = { this.state = {
body: [], body: [],
ast: null, ast: null,
data: 'Hit "Get Data!" or Ctrl + Enter to run your script',
suggestions: [], suggestions: [],
script: `from(db:"foo") script: `fil = (r) => r._measurement == \"cpu\"\ntele = from(db: \"telegraf\") \n\t\t|> filter(fn: fil)\n |> range(start: -1m)\n |> sum()\n\n`,
|> filter(fn: (r) => status: {
(r["a"] == 1 OR r.b == "two") AND type: 'none',
(r["b"] == true OR r.d == "four"))`, text: '',
},
} }
} }
@ -59,39 +85,49 @@ export class IFQLPage extends PureComponent<Props, State> {
} }
public render() { public render() {
const {suggestions, script} = this.state const {suggestions, script, data, body, status} = this.state
return ( return (
<CheckServices>
<IFQLContext.Provider value={this.handlers}> <IFQLContext.Provider value={this.handlers}>
<KeyboardShortcuts onControlEnter={this.handleSubmitScript}> <KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<div className="page hosts-list-page"> <div className="page hosts-list-page">
<div className="page-header full-width"> {this.header}
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Time Machine</h1>
</div>
<div className="page-header__right">
<button
className="btn btn-sm btn-primary"
onClick={this.handleSubmitScript}
>
Submit Script
</button>
</div>
</div>
</div>
<TimeMachine <TimeMachine
data={data}
body={body}
script={script} script={script}
body={this.state.body} status={status}
suggestions={suggestions} suggestions={suggestions}
onAnalyze={this.handleAnalyze}
onAppendFrom={this.handleAppendFrom}
onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript} onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript}
/> />
</div> </div>
</KeyboardShortcuts> </KeyboardShortcuts>
</IFQLContext.Provider> </IFQLContext.Provider>
</CheckServices>
) )
} }
private get header(): JSX.Element {
const {services} = this.props
if (!services.length) {
return null
}
return (
<IFQLHeader service={this.service} onGetTimeSeries={this.getTimeSeries} />
)
}
private get service(): Service {
return this.props.services[0]
}
private get handlers(): Handlers { private get handlers(): Handlers {
return { return {
onAddNode: this.handleAddNode, onAddNode: this.handleAddNode,
@ -223,6 +259,20 @@ export class IFQLPage extends PureComponent<Props, State> {
.join(', ') .join(', ')
} }
private handleAppendFrom = (): void => {
const {script} = this.state
const newScript = `${script.trim()}\n\n${builder.NEW_FROM}\n\n`
this.getASTResponse(newScript)
}
private handleAppendJoin = (): void => {
const {script} = this.state
const newScript = `${script.trim()}\n\n${builder.NEW_JOIN}\n\n`
this.getASTResponse(newScript)
}
private handleChangeScript = (script: string): void => { private handleChangeScript = (script: string): void => {
this.setState({script}) this.setState({script})
} }
@ -325,21 +375,77 @@ export class IFQLPage extends PureComponent<Props, State> {
}, '') }, '')
} }
private handleAnalyze = async () => {
const {links, notify} = this.props
try {
const ast = await getAST({url: links.ast, body: this.state.script})
const body = bodyNodes(ast, this.state.suggestions)
const status = {type: 'success', text: ''}
notify(analyzeSuccess)
this.setState({ast, body, status})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
}
private getASTResponse = async (script: string) => { private getASTResponse = async (script: string) => {
const {links} = this.props const {links} = this.props
try { try {
const ast = await getAST({url: links.ast, body: script}) const ast = await getAST({url: links.ast, body: script})
const body = bodyNodes(ast, this.state.suggestions) const suggestions = this.state.suggestions.map(s => {
this.setState({ast, script, body}) if (s.name === funcNames.JOIN) {
return {
...s,
params: {
tables: 'object',
on: 'array',
fn: 'function',
},
}
}
return s
})
const body = bodyNodes(ast, suggestions)
const status = {type: 'success', text: ''}
this.setState({ast, script, body, status})
} catch (error) { } catch (error) {
console.error('Could not parse AST', error) this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
} }
} }
private getTimeSeries = async () => {
const {script} = this.state
this.setState({data: 'fetching data...'})
try {
const {data} = await getTimeSeries(script)
this.setState({data})
} catch (error) {
this.setState({data: 'Error fetching data'})
console.error('Could not get timeSeries', error)
}
this.getASTResponse(script)
}
private parseError = (error): Status => {
const s = error.data.slice(0, -5) // There is a 'null\n' at the end of these responses
const data = JSON.parse(s)
return {type: 'error', text: `${data.message}`}
}
} }
const mapStateToProps = ({links}) => { const mapStateToProps = ({links, services, sources}) => {
return {links: links.ifql} return {links: links.ifql, services, sources}
} }
export default connect(mapStateToProps, null)(IFQLPage) const mapDispatchToProps = {
notify: notifyAction,
}
export default connect(mapStateToProps, mapDispatchToProps)(IFQLPage)

View File

@ -49,7 +49,17 @@ export const bodyNodes = (ast, suggestions): Body[] => {
const functions = (funcs, suggestions): Func[] => { const functions = (funcs, suggestions): Func[] => {
const funcList = funcs.map(func => { const funcList = funcs.map(func => {
const {params, name} = suggestions.find(f => f.name === func.name) const suggestion = suggestions.find(f => f.name === func.name)
if (!suggestion) {
return {
id: uuid.v4(),
source: func.source,
name: func.name,
args: func.args,
}
}
const {params, name} = suggestion
const args = Object.entries(params).map(([key, type]) => { const args = Object.entries(params).map(([key, type]) => {
const value = _.get(func.args.find(arg => arg.key === key), 'value', '') const value = _.get(func.args.find(arg => arg.key === key), 'value', '')

View File

@ -1,3 +1,4 @@
import IFQLPage from 'src/ifql/containers/IFQLPage' import IFQLPage from 'src/ifql/containers/IFQLPage'
import CheckServices from 'src/ifql/containers/CheckServices'
export {IFQLPage} export {IFQLPage, CheckServices}

View File

@ -35,7 +35,7 @@ import {
} from 'src/kapacitor' } from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin' import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources' import {SourcePage, ManageSources} from 'src/sources'
import {IFQLPage} from 'src/ifql/index' import {IFQLPage} from 'src/ifql'
import NotFound from 'src/shared/components/NotFound' import NotFound from 'src/shared/components/NotFound'
import {getLinksAsync} from 'src/shared/actions/links' import {getLinksAsync} from 'src/shared/actions/links'

View File

@ -5,9 +5,10 @@ interface Props {
label: string label: string
value: string value: string
placeholder: string placeholder: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
maxLength?: number maxLength?: number
inputType?: string inputType?: string
customClass?: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
} }
const KapacitorFormInput: SFC<Props> = ({ const KapacitorFormInput: SFC<Props> = ({
@ -18,8 +19,9 @@ const KapacitorFormInput: SFC<Props> = ({
onChange, onChange,
maxLength, maxLength,
inputType, inputType,
customClass,
}) => ( }) => (
<div className="form-group"> <div className={`form-group ${customClass}`}>
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
<input <input
className="form-control" className="form-control"
@ -37,6 +39,7 @@ const KapacitorFormInput: SFC<Props> = ({
KapacitorFormInput.defaultProps = { KapacitorFormInput.defaultProps = {
inputType: '', inputType: '',
customClass: 'col-sm-6',
} }
export default KapacitorFormInput export default KapacitorFormInput

View File

@ -169,6 +169,7 @@ export class KapacitorPage extends PureComponent<Props, State> {
return ( return (
<KapacitorForm <KapacitorForm
hash={hash} hash={hash}
notify={notify}
source={source} source={source}
exists={exists} exists={exists}
kapacitor={kapacitor} kapacitor={kapacitor}
@ -176,7 +177,6 @@ export class KapacitorPage extends PureComponent<Props, State> {
onChangeUrl={this.handleChangeUrl} onChangeUrl={this.handleChangeUrl}
onReset={this.handleResetToDefaults} onReset={this.handleResetToDefaults}
onInputChange={this.handleInputChange} onInputChange={this.handleInputChange}
notify={notify}
onCheckboxChange={this.handleCheckboxChange} onCheckboxChange={this.handleCheckboxChange}
/> />
) )

View File

@ -8,6 +8,19 @@ interface Options {
transitionTime?: number transitionTime?: number
} }
export type ShowOverlay = (
OverlayNode: OverlayNodeType,
options: Options
) => ActionOverlayNode
export interface ActionOverlayNode {
type: 'SHOW_OVERLAY'
payload: {
OverlayNode
options
}
}
export const showOverlay = ( export const showOverlay = (
OverlayNode: OverlayNodeType, OverlayNode: OverlayNodeType,
options: Options options: Options

View File

@ -0,0 +1,148 @@
import {Source, Service, NewService} from 'src/types'
import {
updateService as updateServiceAJAX,
getServices as getServicesAJAX,
createService as createServiceAJAX,
} from 'src/shared/apis'
import {notify} from './notifications'
import {couldNotGetServices} from 'src/shared/copy/notifications'
export type Action =
| ActionLoadServices
| ActionAddService
| ActionDeleteService
| ActionUpdateService
| ActionSetActiveService
// Load Services
export type LoadServices = (services: Service[]) => ActionLoadServices
export interface ActionLoadServices {
type: 'LOAD_SERVICES'
payload: {
services: Service[]
}
}
export const loadServices = (services: Service[]): ActionLoadServices => ({
type: 'LOAD_SERVICES',
payload: {
services,
},
})
// Add a Service
export type AddService = (service: Service) => ActionAddService
export interface ActionAddService {
type: 'ADD_SERVICE'
payload: {
service: Service
}
}
export const addService = (service: Service): ActionAddService => ({
type: 'ADD_SERVICE',
payload: {
service,
},
})
// Delete Service
export type DeleteService = (service: Service) => ActionDeleteService
export interface ActionDeleteService {
type: 'DELETE_SERVICE'
payload: {
service: Service
}
}
export const deleteService = (service: Service): ActionDeleteService => ({
type: 'DELETE_SERVICE',
payload: {
service,
},
})
// Update Service
export type UpdateService = (service: Service) => ActionUpdateService
export interface ActionUpdateService {
type: 'UPDATE_SERVICE'
payload: {
service: Service
}
}
export const updateService = (service: Service): ActionUpdateService => ({
type: 'UPDATE_SERVICE',
payload: {
service,
},
})
// Set Active Service
export type SetActiveService = (
source: Source,
service: Service
) => ActionSetActiveService
export interface ActionSetActiveService {
type: 'SET_ACTIVE_SERVICE'
payload: {
source: Source
service: Service
}
}
export const setActiveService = (
source: Source,
service: Service
): ActionSetActiveService => ({
type: 'SET_ACTIVE_SERVICE',
payload: {
source,
service,
},
})
export type FetchServicesAsync = (source: Source) => (dispatch) => Promise<void>
export const fetchServicesAsync = (source: Source) => async (
dispatch
): Promise<void> => {
try {
const services = await getServicesAJAX(source.links.services)
dispatch(loadServices(services))
} catch (err) {
dispatch(notify(couldNotGetServices))
}
}
export type CreateServiceAsync = (
source: Source,
service: NewService
) => (dispatch) => Promise<void>
export const createServiceAsync = (
source: Source,
service: NewService
) => async (dispatch): Promise<void> => {
try {
const s = await createServiceAJAX(source, service)
dispatch(addService(s))
} catch (err) {
console.error(err.data)
throw err.data
}
}
export type UpdateServiceAsync = (
service: Service
) => (dispatch) => Promise<void>
export const updateServiceAsync = (service: Service) => async (
dispatch
): Promise<void> => {
try {
const s = await updateServiceAJAX(service)
dispatch(updateService(s))
} catch (err) {
console.error(err.data)
throw err.data
}
}

View File

@ -1,120 +0,0 @@
import {
deleteSource,
getSources as getSourcesAJAX,
getKapacitors as getKapacitorsAJAX,
updateKapacitor as updateKapacitorAJAX,
deleteKapacitor as deleteKapacitorAJAX,
} from 'shared/apis'
import {notify} from './notifications'
import {errorThrown} from 'shared/actions/errors'
import {HTTP_NOT_FOUND} from 'shared/constants'
import {
notifyServerError,
notifyCouldNotRetrieveKapacitors,
notifyCouldNotDeleteKapacitor,
} from 'shared/copy/notifications'
export const loadSources = sources => ({
type: 'LOAD_SOURCES',
payload: {
sources,
},
})
export const updateSource = source => ({
type: 'SOURCE_UPDATED',
payload: {
source,
},
})
export const addSource = source => ({
type: 'SOURCE_ADDED',
payload: {
source,
},
})
export const fetchKapacitors = (source, kapacitors) => ({
type: 'LOAD_KAPACITORS',
payload: {
source,
kapacitors,
},
})
export const setActiveKapacitor = kapacitor => ({
type: 'SET_ACTIVE_KAPACITOR',
payload: {
kapacitor,
},
})
export const deleteKapacitor = kapacitor => ({
type: 'DELETE_KAPACITOR',
payload: {
kapacitor,
},
})
// Async action creators
export const removeAndLoadSources = source => async dispatch => {
try {
try {
await deleteSource(source)
} catch (err) {
// A 404 means that either a concurrent write occurred or the source
// passed to this action creator doesn't exist (or is undefined)
if (err.status !== HTTP_NOT_FOUND) {
// eslint-disable-line no-magic-numbers
throw err
}
}
const {
data: {sources: newSources},
} = await getSourcesAJAX()
dispatch(loadSources(newSources))
} catch (err) {
dispatch(notify(notifyServerError()))
}
}
export const fetchKapacitorsAsync = source => async dispatch => {
try {
const {data} = await getKapacitorsAJAX(source)
dispatch(fetchKapacitors(source, data.kapacitors))
} catch (err) {
dispatch(notify(notifyCouldNotRetrieveKapacitors(source.id)))
}
}
export const setActiveKapacitorAsync = kapacitor => async dispatch => {
// eagerly update the redux state
dispatch(setActiveKapacitor(kapacitor))
const kapacitorPost = {...kapacitor, active: true}
await updateKapacitorAJAX(kapacitorPost)
}
export const deleteKapacitorAsync = kapacitor => async dispatch => {
try {
await deleteKapacitorAJAX(kapacitor)
dispatch(deleteKapacitor(kapacitor))
} catch (err) {
dispatch(notify(notifyCouldNotDeleteKapacitor()))
}
}
export const getSourcesAsync = () => async dispatch => {
try {
const {
data: {sources},
} = await getSourcesAJAX()
dispatch(loadSources(sources))
return sources
} catch (error) {
dispatch(errorThrown(error))
}
}

View File

@ -0,0 +1,215 @@
import {
deleteSource,
getSources as getSourcesAJAX,
getKapacitors as getKapacitorsAJAX,
updateKapacitor as updateKapacitorAJAX,
deleteKapacitor as deleteKapacitorAJAX,
} from 'src/shared/apis'
import {notify} from './notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {HTTP_NOT_FOUND} from 'src/shared/constants'
import {
notifyServerError,
notifyCouldNotRetrieveKapacitors,
notifyCouldNotDeleteKapacitor,
} from 'src/shared/copy/notifications'
import {Source, Kapacitor} from 'src/types'
export type Action =
| ActionLoadSources
| ActionUpdateSource
| ActionAddSource
| ActionFetchKapacitors
| ActionSetActiveKapacitor
| ActionDeleteKapacitor
// Load Sources
export type LoadSources = (sources: Source[]) => ActionLoadSources
export interface ActionLoadSources {
type: 'LOAD_SOURCES'
payload: {
sources: Source[]
}
}
export const loadSources = (sources: Source[]): ActionLoadSources => ({
type: 'LOAD_SOURCES',
payload: {
sources,
},
})
export type UpdateSource = (source: Source) => ActionUpdateSource
export interface ActionUpdateSource {
type: 'SOURCE_UPDATED'
payload: {
source: Source
}
}
export const updateSource = (source: Source): ActionUpdateSource => ({
type: 'SOURCE_UPDATED',
payload: {
source,
},
})
export type AddSource = (source: Source) => ActionAddSource
export interface ActionAddSource {
type: 'SOURCE_ADDED'
payload: {
source: Source
}
}
export const addSource = (source: Source): ActionAddSource => ({
type: 'SOURCE_ADDED',
payload: {
source,
},
})
export type FetchKapacitors = (
source: Source,
kapacitors: Kapacitor[]
) => ActionFetchKapacitors
export interface ActionFetchKapacitors {
type: 'LOAD_KAPACITORS'
payload: {
source: Source
kapacitors: Kapacitor[]
}
}
export const fetchKapacitors = (
source: Source,
kapacitors: Kapacitor[]
): ActionFetchKapacitors => ({
type: 'LOAD_KAPACITORS',
payload: {
source,
kapacitors,
},
})
export type SetActiveKapacitor = (
kapacitor: Kapacitor
) => ActionSetActiveKapacitor
export interface ActionSetActiveKapacitor {
type: 'SET_ACTIVE_KAPACITOR'
payload: {
kapacitor: Kapacitor
}
}
export const setActiveKapacitor = (
kapacitor: Kapacitor
): ActionSetActiveKapacitor => ({
type: 'SET_ACTIVE_KAPACITOR',
payload: {
kapacitor,
},
})
export type DeleteKapacitor = (kapacitor: Kapacitor) => ActionDeleteKapacitor
export interface ActionDeleteKapacitor {
type: 'DELETE_KAPACITOR'
payload: {
kapacitor: Kapacitor
}
}
export const deleteKapacitor = (kapacitor: Kapacitor) => ({
type: 'DELETE_KAPACITOR',
payload: {
kapacitor,
},
})
export type RemoveAndLoadSources = (
source: Source
) => (dispatch) => Promise<void>
// Async action creators
export const removeAndLoadSources = (source: Source) => async (
dispatch
): Promise<void> => {
try {
try {
await deleteSource(source)
} catch (err) {
// A 404 means that either a concurrent write occurred or the source
// passed to this action creator doesn't exist (or is undefined)
if (err.status !== HTTP_NOT_FOUND) {
// eslint-disable-line no-magic-numbers
throw err
}
}
const {
data: {sources: newSources},
} = await getSourcesAJAX()
dispatch(loadSources(newSources))
} catch (err) {
dispatch(notify(notifyServerError()))
}
}
export type FetchKapacitorsAsync = (
source: Source
) => (dispatch) => Promise<void>
export const fetchKapacitorsAsync = (source: Source) => async (
dispatch
): Promise<void> => {
try {
const {data} = await getKapacitorsAJAX(source)
dispatch(fetchKapacitors(source, data.kapacitors))
} catch (err) {
dispatch(notify(notifyCouldNotRetrieveKapacitors(source.id)))
}
}
export type SetActiveKapacitorAsync = (
source: Source
) => (dispatch) => Promise<void>
export const setActiveKapacitorAsync = (kapacitor: Kapacitor) => async (
dispatch
): Promise<void> => {
// eagerly update the redux state
dispatch(setActiveKapacitor(kapacitor))
const kapacitorPost = {...kapacitor, active: true}
await updateKapacitorAJAX(kapacitorPost)
}
export type DeleteKapacitorAsync = (
source: Source
) => (dispatch) => Promise<void>
export const deleteKapacitorAsync = (kapacitor: Kapacitor) => async (
dispatch
): Promise<void> => {
try {
await deleteKapacitorAJAX(kapacitor)
dispatch(deleteKapacitor(kapacitor))
} catch (err) {
dispatch(notify(notifyCouldNotDeleteKapacitor()))
}
}
export const getSourcesAsync = () => async (dispatch): Promise<void> => {
try {
const {
data: {sources},
} = await getSourcesAJAX()
dispatch(loadSources(sources))
return sources
} catch (error) {
dispatch(errorThrown(error))
}
}

View File

@ -1,14 +1,17 @@
import AJAX from 'utils/ajax' import AJAX from 'src/utils/ajax'
import {AlertTypes} from 'src/kapacitor/constants' import {AlertTypes} from 'src/kapacitor/constants'
import {Kapacitor, Source, Service, NewService} from 'src/types'
export function getSources() { export function getSources() {
return AJAX({ return AJAX({
url: null,
resource: 'sources', resource: 'sources',
}) })
} }
export function getSource(id) { export function getSource(id) {
return AJAX({ return AJAX({
url: null,
resource: 'sources', resource: 'sources',
id, id,
}) })
@ -16,6 +19,7 @@ export function getSource(id) {
export function createSource(attributes) { export function createSource(attributes) {
return AJAX({ return AJAX({
url: null,
resource: 'sources', resource: 'sources',
method: 'POST', method: 'POST',
data: attributes, data: attributes,
@ -123,12 +127,12 @@ export function createKapacitor(
export function updateKapacitor({ export function updateKapacitor({
links, links,
url, url,
name = 'My Kapacitor', name = 'My Kaacitor',
username, username,
password, password,
active, active,
insecureSkipVerify, insecureSkipVerify,
}) { }: Kapacitor) {
return AJAX({ return AJAX({
url: links.self, url: links.self,
method: 'PATCH', method: 'PATCH',
@ -282,7 +286,7 @@ export function deleteKapacitorTask(kapacitor, id) {
return kapacitorProxy(kapacitor, 'DELETE', `/kapacitor/v1/tasks/${id}`, '') return kapacitorProxy(kapacitor, 'DELETE', `/kapacitor/v1/tasks/${id}`, '')
} }
export function kapacitorProxy(kapacitor, method, path, body) { export function kapacitorProxy(kapacitor, method, path, body?) {
return AJAX({ return AJAX({
method, method,
url: kapacitor.links.proxy, url: kapacitor.links.proxy,
@ -293,9 +297,63 @@ export function kapacitorProxy(kapacitor, method, path, body) {
}) })
} }
export const getQueryConfigAndStatus = (url, queries, tempVars) => export const getQueryConfigAndStatus = (url, queries, tempVars = []) =>
AJAX({ AJAX({
url, url,
method: 'POST', method: 'POST',
data: {queries, tempVars}, data: {queries, tempVars},
}) })
export const getServices = async (url: string): Promise<Service[]> => {
try {
const {data} = await AJAX({
url,
method: 'GET',
})
return data.services
} catch (error) {
console.error(error)
throw error
}
}
export const createService = async (
source: Source,
{
url,
name = 'My IFQLD',
type,
username,
password,
insecureSkipVerify,
}: NewService
): Promise<Service> => {
try {
const {data} = await AJAX({
url: source.links.services,
method: 'POST',
data: {url, name, type, username, password, insecureSkipVerify},
})
return data
} catch (error) {
console.error(error)
throw error
}
}
export const updateService = async (service: Service): Promise<Service> => {
try {
const {data} = await AJAX({
url: service.links.self,
method: 'PATCH',
data: service,
})
return data
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -2,13 +2,16 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import calculateSize from 'calculate-size' import calculateSize from 'calculate-size'
import DivisionHeader from 'src/shared/components/threesizer/DivisionHeader'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
import {MenuItem} from 'src/shared/components/threesizer/DivisionMenu'
const NOOP = () => {} const NOOP = () => {}
interface Props { interface Props {
name?: string name?: string
handleDisplay?: string handleDisplay?: string
menuOptions?: MenuItem[]
handlePixels: number handlePixels: number
id: string id: string
size: number size: number
@ -19,6 +22,9 @@ interface Props {
render: (visibility: string) => ReactElement<any> render: (visibility: string) => ReactElement<any>
onHandleStartDrag: (id: string, e: MouseEvent<HTMLElement>) => void onHandleStartDrag: (id: string, e: MouseEvent<HTMLElement>) => void
onDoubleClick: (id: string) => void onDoubleClick: (id: string) => void
onMaximize: (id: string) => void
onMinimize: (id: string) => void
headerButtons: JSX.Element[]
} }
interface Style { interface Style {
@ -59,7 +65,7 @@ class Division extends PureComponent<Props> {
} }
public render() { public render() {
const {name, render, draggable} = this.props const {name, render, draggable, menuOptions, headerButtons} = this.props
return ( return (
<div <div
className={this.containerClass} className={this.containerClass}
@ -77,7 +83,14 @@ class Division extends PureComponent<Props> {
<div className={this.titleClass}>{name}</div> <div className={this.titleClass}>{name}</div>
</div> </div>
<div className={this.contentsClass} style={this.contentStyle}> <div className={this.contentsClass} style={this.contentStyle}>
{name && <div className="threesizer--header" />} {name && (
<DivisionHeader
buttons={headerButtons}
menuOptions={menuOptions}
onMinimize={this.handleMinimize}
onMaximize={this.handleMaximize}
/>
)}
<div className="threesizer--body">{render(this.visibility)}</div> <div className="threesizer--body">{render(this.visibility)}</div>
</div> </div>
</div> </div>
@ -162,7 +175,10 @@ class Division extends PureComponent<Props> {
private get handleClass(): string { private get handleClass(): string {
const {draggable, orientation} = this.props const {draggable, orientation} = this.props
const collapsed = orientation === HANDLE_VERTICAL && this.isTitleObscured
return classnames('threesizer--handle', { return classnames('threesizer--handle', {
'threesizer--collapsed': collapsed,
disabled: !draggable, disabled: !draggable,
dragging: this.isDragging, dragging: this.isDragging,
vertical: orientation === HANDLE_VERTICAL, vertical: orientation === HANDLE_VERTICAL,
@ -223,6 +239,16 @@ class Division extends PureComponent<Props> {
onDoubleClick(id) onDoubleClick(id)
} }
private handleMinimize = (): void => {
const {id, onMinimize} = this.props
onMinimize(id)
}
private handleMaximize = (): void => {
const {id, onMaximize} = this.props
onMaximize(id)
}
} }
export default Division export default Division

View File

@ -0,0 +1,39 @@
import React, {PureComponent} from 'react'
import DivisionMenu, {
MenuItem,
} from 'src/shared/components/threesizer/DivisionMenu'
interface Props {
onMinimize: () => void
onMaximize: () => void
buttons: JSX.Element[]
menuOptions?: MenuItem[]
}
class DivisionHeader extends PureComponent<Props> {
public render() {
return (
<div className="threesizer--header">
{this.props.buttons.map(b => b)}
<DivisionMenu menuItems={this.menuItems} />
</div>
)
}
private get menuItems(): MenuItem[] {
const {onMaximize, onMinimize, menuOptions} = this.props
return [
...menuOptions,
{
action: onMaximize,
text: 'Maximize',
},
{
action: onMinimize,
text: 'Minimize',
},
]
}
}
export default DivisionHeader

View File

@ -0,0 +1,88 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
export interface MenuItem {
text: string
action: () => void
}
interface Props {
menuItems: MenuItem[]
}
interface State {
expanded: boolean
}
class DivisionMenu extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
expanded: false,
}
}
public render() {
const {expanded} = this.state
return (
<ClickOutside onClickOutside={this.handleCollapseMenu}>
<div className={this.menuClass}>
<button className={this.buttonClass} onClick={this.handleExpandMenu}>
<span className="icon caret-down" />
</button>
{expanded && this.renderMenu}
</div>
</ClickOutside>
)
}
private handleExpandMenu = (): void => {
this.setState({expanded: true})
}
private handleCollapseMenu = (): void => {
this.setState({expanded: false})
}
private handleMenuItemClick = action => (): void => {
this.setState({expanded: false})
action()
}
private get menuClass(): string {
const {expanded} = this.state
return classnames('dropdown threesizer--menu', {open: expanded})
}
private get buttonClass(): string {
const {expanded} = this.state
return classnames('btn btn-sm btn-square btn-default', {
active: expanded,
})
}
private get renderMenu(): JSX.Element {
const {menuItems} = this.props
return (
<ul className="dropdown-menu">
{menuItems.map(item => (
<li
key={uuid.v4()}
className="dropdown-item"
onClick={this.handleMenuItemClick(item.action)}
>
<a href="#">{item.text}</a>
</li>
))}
</ul>
)
}
}
export default DivisionMenu

View File

@ -3,8 +3,10 @@ import classnames from 'classnames'
import uuid from 'uuid' import uuid from 'uuid'
import _ from 'lodash' import _ from 'lodash'
import ResizeDivision from 'src/shared/components/ResizeDivision' import Division from 'src/shared/components/threesizer/Division'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {MenuItem} from 'src/shared/components/threesizer/DivisionMenu'
import { import {
HANDLE_NONE, HANDLE_NONE,
HANDLE_PIXELS, HANDLE_PIXELS,
@ -28,20 +30,22 @@ interface State {
dragEvent: any dragEvent: any
} }
interface Division { interface DivisionProps {
name?: string name?: string
handleDisplay?: string handleDisplay?: string
handlePixels?: number handlePixels?: number
headerButtons?: JSX.Element[]
menuOptions: MenuItem[]
render: (visibility?: string) => ReactElement<any> render: (visibility?: string) => ReactElement<any>
} }
interface DivisionState extends Division { interface DivisionState extends DivisionProps {
id: string id: string
size: number size: number
} }
interface Props { interface Props {
divisions: Division[] divisions: DivisionProps[]
orientation: string orientation: string
containerClass?: string containerClass?: string
} }
@ -129,7 +133,7 @@ class Threesizer extends Component<Props, State> {
ref={r => (this.containerRef = r)} ref={r => (this.containerRef = r)}
> >
{divisions.map((d, i) => ( {divisions.map((d, i) => (
<ResizeDivision <Division
key={d.id} key={d.id}
id={d.id} id={d.id}
name={d.name} name={d.name}
@ -140,9 +144,13 @@ class Threesizer extends Component<Props, State> {
handlePixels={d.handlePixels} handlePixels={d.handlePixels}
handleDisplay={d.handleDisplay} handleDisplay={d.handleDisplay}
activeHandleID={activeHandleID} activeHandleID={activeHandleID}
onMaximize={this.handleMaximize}
onMinimize={this.handleMinimize}
onDoubleClick={this.handleDoubleClick} onDoubleClick={this.handleDoubleClick}
onHandleStartDrag={this.handleStartDrag}
render={this.props.divisions[i].render} render={this.props.divisions[i].render}
onHandleStartDrag={this.handleStartDrag}
menuOptions={this.props.divisions[i].menuOptions}
headerButtons={this.props.divisions[i].headerButtons}
/> />
))} ))}
</div> </div>
@ -209,6 +217,50 @@ class Threesizer extends Component<Props, State> {
this.setState({divisions}) this.setState({divisions})
} }
private handleMaximize = (id: string): void => {
const maxDiv = this.state.divisions.find(d => d.id === id)
if (!maxDiv) {
return
}
const divisions = this.state.divisions.map(d => {
if (d.id !== id) {
return {...d, size: 0}
}
return {...d, size: 1}
})
this.setState({divisions})
}
private handleMinimize = (id: string): void => {
const minDiv = this.state.divisions.find(d => d.id === id)
const numDivisions = this.state.divisions.length
if (!minDiv) {
return
}
let size
if (numDivisions <= 1) {
size = 1
} else {
size = 1 / (this.state.divisions.length - 1)
}
const divisions = this.state.divisions.map(d => {
if (d.id !== id) {
return {...d, size}
}
return {...d, size: 0}
})
this.setState({divisions})
}
private equalize = () => { private equalize = () => {
const denominator = this.state.divisions.length const denominator = this.state.divisions.length
const divisions = this.state.divisions.map(d => { const divisions = this.state.divisions.map(d => {

View File

@ -1,7 +1,7 @@
// All copy for notifications should be stored here for easy editing // All copy for notifications should be stored here for easy editing
// and ensuring stylistic consistency // and ensuring stylistic consistency
import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'shared/constants/index' import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index'
const defaultErrorNotification = { const defaultErrorNotification = {
type: 'error', type: 'error',
@ -131,7 +131,7 @@ export const notifySourceUdpateFailed = (sourceName, errorMessage) => ({
message: `Failed to update InfluxDB ${sourceName} Connection: ${errorMessage}`, message: `Failed to update InfluxDB ${sourceName} Connection: ${errorMessage}`,
}) })
export const notifySourceDeleted = sourceName => ({ export const notifySourceDeleted = (sourceName: string) => ({
...defaultSuccessNotification, ...defaultSuccessNotification,
icon: 'server2', icon: 'server2',
message: `${sourceName} deleted successfully.`, message: `${sourceName} deleted successfully.`,
@ -536,7 +536,7 @@ export const notifyTestAlertSent = endpoint => ({
message: `Test Alert sent to ${endpoint}. If the Alert does not reach its destination, please check your endpoint configuration settings.`, message: `Test Alert sent to ${endpoint}. If the Alert does not reach its destination, please check your endpoint configuration settings.`,
}) })
export const notifyTestAlertFailed = (endpoint, errorMessage) => ({ export const notifyTestAlertFailed = (endpoint, errorMessage?) => ({
...defaultErrorNotification, ...defaultErrorNotification,
message: `There was an error sending a Test Alert to ${endpoint}${ message: `There was an error sending a Test Alert to ${endpoint}${
errorMessage ? `: ${errorMessage}` : '.' errorMessage ? `: ${errorMessage}` : '.'
@ -608,3 +608,35 @@ export const notifyKapacitorNotFound = () => ({
...defaultErrorNotification, ...defaultErrorNotification,
message: 'We could not find a Kapacitor configuration for this source.', message: 'We could not find a Kapacitor configuration for this source.',
}) })
// IFQL notifications
export const analyzeSuccess = {
...defaultSuccessNotification,
message: 'No errors found. Happy Happy Joy Joy!',
}
// Service notifications
export const couldNotGetServices = {
...defaultErrorNotification,
message: 'We could not get services',
}
export const ifqlCreated = {
...defaultSuccessNotification,
message: 'IFQL Connection Created. Script your heart out!',
}
export const ifqlNotCreated = (message: string) => ({
...defaultErrorNotification,
message,
})
export const ifqlNotUpdated = (message: string) => ({
...defaultErrorNotification,
message,
})
export const ifqlUpdated = {
...defaultSuccessNotification,
message: 'Connection Updated. Rejoice!',
}

View File

@ -0,0 +1,43 @@
import {Action} from 'src/shared/actions/services'
import {Service} from 'src/types'
export const initialState: Service[] = []
const servicesReducer = (state = initialState, action: Action): Service[] => {
switch (action.type) {
case 'LOAD_SERVICES': {
return action.payload.services
}
case 'ADD_SERVICE': {
const {service} = action.payload
return [...state, service]
}
case 'DELETE_SERVICE': {
const {service} = action.payload
return state.filter(s => s.id !== service.id)
}
case 'UPDATE_SERVICE': {
const {service} = action.payload
const newState = state.map(s => {
if (s.id === service.id) {
return {...s, ...service}
}
return {...s}
})
return newState
}
case 'SET_ACTIVE_SERVICE': {
}
}
return state
}
export default servicesReducer

View File

@ -1,10 +1,10 @@
import _ from 'lodash' import _ from 'lodash'
import {Source, Kapacitor} from 'src/types'
import {Action} from 'src/shared/actions/sources'
const getInitialState = () => [] export const initialState: Source[] = []
const initialState = getInitialState() const sourcesReducer = (state = initialState, action: Action): Source[] => {
const sourcesReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'LOAD_SOURCES': { case 'LOAD_SOURCES': {
return action.payload.sources return action.payload.sources
@ -59,7 +59,11 @@ const sourcesReducer = (state = initialState, action) => {
const {kapacitor} = action.payload const {kapacitor} = action.payload
const updatedSources = _.cloneDeep(state) const updatedSources = _.cloneDeep(state)
updatedSources.forEach(source => { updatedSources.forEach(source => {
const index = _.findIndex(source.kapacitors, k => k.id === kapacitor.id) const index = _.findIndex<Kapacitor>(
source.kapacitors,
k => k.id === kapacitor.id
)
if (index >= 0) { if (index >= 0) {
source.kapacitors.splice(index, 1) source.kapacitors.splice(index, 1)
} }

View File

@ -0,0 +1,56 @@
import React, {PureComponent} from 'react'
import {Link} from 'react-router'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Source} from 'src/types'
interface Props {
source: Source
currentSource: Source
}
class ConnectionLink extends PureComponent<Props> {
public render() {
const {source} = this.props
return (
<h5 className="margin-zero">
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={<strong>{source.name}</strong>}
>
<Link
to={`${location.pathname}/${source.id}/edit`}
className={this.className}
>
<strong>{source.name}</strong>
{this.default}
</Link>
</Authorized>
</h5>
)
}
private get className(): string {
if (this.isCurrentSource) {
return 'link-success'
}
return ''
}
private get default(): string {
const {source} = this.props
if (source.default) {
return ' (Default)'
}
return ''
}
private get isCurrentSource(): boolean {
const {source, currentSource} = this.props
return source.id === currentSource.id
}
}
export default ConnectionLink

View File

@ -1,247 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link, withRouter} from 'react-router'
import {connect} from 'react-redux'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import Dropdown from 'shared/components/Dropdown'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
import ConfirmButton from 'shared/components/ConfirmButton'
const kapacitorDropdown = (
kapacitors,
source,
router,
setActiveKapacitor,
handleDeleteKapacitor
) => {
if (!kapacitors || kapacitors.length === 0) {
return (
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/kapacitors/new`}
className="btn btn-xs btn-default"
>
<span className="icon plus" /> Add Kapacitor Connection
</Link>
</Authorized>
)
}
const kapacitorItems = kapacitors.map(k => {
return {
text: k.name,
resource: `/sources/${source.id}/kapacitors/${k.id}`,
kapacitor: k,
}
})
const activeKapacitor = kapacitors.find(k => k.active)
let selected = ''
if (activeKapacitor) {
selected = activeKapacitor.name
} else {
selected = kapacitorItems[0].text
}
const unauthorizedDropdown = (
<div className="source-table--kapacitor__view-only">{selected}</div>
)
return (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={unauthorizedDropdown}
>
<Dropdown
className="dropdown-260"
buttonColor="btn-primary"
buttonSize="btn-xs"
items={kapacitorItems}
onChoose={setActiveKapacitor}
addNew={{
url: `/sources/${source.id}/kapacitors/new`,
text: 'Add Kapacitor Connection',
}}
actions={[
{
icon: 'pencil',
text: 'edit',
handler: item => {
router.push(`${item.resource}/edit`)
},
},
{
icon: 'trash',
text: 'delete',
handler: item => {
handleDeleteKapacitor(item.kapacitor)
},
confirmable: true,
},
]}
selected={selected}
/>
</Authorized>
)
}
const InfluxTable = ({
source,
router,
sources,
location,
setActiveKapacitor,
handleDeleteSource,
handleDeleteKapacitor,
isUsingAuth,
me,
}) => {
return (
<div className="row">
<div className="col-md-12">
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">
{isUsingAuth ? (
<span>
Connections for <em>{me.currentOrganization.name}</em>
</span>
) : (
<span>Connections</span>
)}
</h2>
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/manage-sources/new`}
className="btn btn-sm btn-primary"
>
<span className="icon plus" /> Add Connection
</Link>
</Authorized>
</div>
<div className="panel-body">
<table className="table v-center margin-bottom-zero table-highlight">
<thead>
<tr>
<th className="source-table--connect-col" />
<th>InfluxDB Connection</th>
<th className="text-right" />
<th>
Kapacitor Connection{' '}
<QuestionMarkTooltip
tipID="kapacitor-node-helper"
tipContent={
'<p>Kapacitor Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'
}
/>
</th>
</tr>
</thead>
<tbody>
{sources.map(s => {
return (
<tr
key={s.id}
className={s.id === source.id ? 'highlight' : null}
>
<td>
{s.id === source.id ? (
<div className="btn btn-success btn-xs source-table--connect">
Connected
</div>
) : (
<Link
className="btn btn-default btn-xs source-table--connect"
to={`/sources/${s.id}/hosts`}
>
Connect
</Link>
)}
</td>
<td>
<h5 className="margin-zero">
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={
<strong>{s.name}</strong>
}
>
<Link
to={`${location.pathname}/${s.id}/edit`}
className={
s.id === source.id ? 'link-success' : null
}
>
<strong>{s.name}</strong>
{s.default ? ' (Default)' : null}
</Link>
</Authorized>
</h5>
<span>{s.url}</span>
</td>
<td className="text-right">
<Authorized requiredRole={EDITOR_ROLE}>
<ConfirmButton
customClass="delete-source table--show-on-row-hover"
type="btn-danger"
size="btn-xs"
text="Delete Connection"
confirmAction={handleDeleteSource(s)}
/>
</Authorized>
</td>
<td className="source-table--kapacitor">
{kapacitorDropdown(
s.kapacitors,
s,
router,
setActiveKapacitor,
handleDeleteKapacitor
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}
const {array, bool, func, shape, string} = PropTypes
InfluxTable.propTypes = {
handleDeleteSource: func.isRequired,
location: shape({
pathname: string.isRequired,
}).isRequired,
router: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
source: shape({
id: string.isRequired,
links: shape({
proxy: string.isRequired,
self: string.isRequired,
}),
}),
sources: array.isRequired,
setActiveKapacitor: func.isRequired,
handleDeleteKapacitor: func.isRequired,
me: shape({
currentOrganization: shape({
id: string.isRequired,
name: string.isRequired,
}),
}),
isUsingAuth: bool,
}
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})
export default connect(mapStateToProps)(withRouter(InfluxTable))

View File

@ -0,0 +1,71 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {SetActiveKapacitor, DeleteKapacitor} from 'src/shared/actions/sources'
import InfluxTableHead from 'src/sources/components/InfluxTableHead'
import InfluxTableHeader from 'src/sources/components/InfluxTableHeader'
import InfluxTableRow from 'src/sources/components/InfluxTableRow'
import {Source, Me} from 'src/types'
interface Props {
me: Me
source: Source
sources: Source[]
isUsingAuth: boolean
deleteKapacitor: DeleteKapacitor
setActiveKapacitor: SetActiveKapacitor
onDeleteSource: (source: Source) => () => void
}
class InfluxTable extends PureComponent<Props> {
public render() {
const {
source,
sources,
setActiveKapacitor,
onDeleteSource,
deleteKapacitor,
isUsingAuth,
me,
} = this.props
return (
<div className="row">
<div className="col-md-12">
<div className="panel">
<InfluxTableHeader
me={me}
source={source}
isUsingAuth={isUsingAuth}
/>
<div className="panel-body">
<table className="table v-center margin-bottom-zero table-highlight">
<InfluxTableHead />
<tbody>
{sources.map(s => {
return (
<InfluxTableRow
key={s.id}
source={s}
currentSource={source}
onDeleteSource={onDeleteSource}
deleteKapacitor={deleteKapacitor}
setActiveKapacitor={setActiveKapacitor}
/>
)
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}
}
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})
export default connect(mapStateToProps)(InfluxTable)

View File

@ -0,0 +1,28 @@
import React, {SFC, ReactElement} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {KAPACITOR_TOOLTIP_COPY} from 'src/sources/constants'
const InfluxTableHead: SFC<{}> = (): ReactElement<
HTMLTableHeaderCellElement
> => {
return (
<thead>
<tr>
<th className="source-table--connect-col" />
<th>InfluxDB Connection</th>
<th className="text-right" />
<th>
Kapacitor Connection
<QuestionMarkTooltip
tipID="kapacitor-node-helper"
tipContent={KAPACITOR_TOOLTIP_COPY}
/>
</th>
</tr>
</thead>
)
}
export default InfluxTableHead

View File

@ -0,0 +1,47 @@
import React, {PureComponent, ReactElement} from 'react'
import {Link} from 'react-router'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Me, Source} from 'src/types'
interface Props {
me: Me
source: Source
isUsingAuth: boolean
}
class InfluxTableHeader extends PureComponent<Props> {
public render() {
const {source} = this.props
return (
<div className="panel-heading">
<h2 className="panel-title">{this.title}</h2>
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/manage-sources/new`}
className="btn btn-sm btn-primary"
>
<span className="icon plus" /> Add Connection
</Link>
</Authorized>
</div>
)
}
private get title(): ReactElement<HTMLSpanElement> {
const {isUsingAuth, me} = this.props
if (isUsingAuth) {
return (
<span>
Connections for <em>{me.currentOrganization.name}</em>
</span>
)
}
return <span>Connections</span>
}
}
export default InfluxTableHeader

View File

@ -0,0 +1,98 @@
import React, {PureComponent, ReactElement} from 'react'
import {Link} from 'react-router'
import * as actions from 'src/shared/actions/sources'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import KapacitorDropdown from 'src/sources/components/KapacitorDropdown'
import ConnectionLink from 'src/sources/components/ConnectionLink'
import {Source} from 'src/types'
interface Props {
source: Source
currentSource: Source
onDeleteSource: (source: Source) => void
setActiveKapacitor: actions.SetActiveKapacitor
deleteKapacitor: actions.DeleteKapacitor
}
class InfluxTableRow extends PureComponent<Props> {
public render() {
const {
source,
currentSource,
setActiveKapacitor,
deleteKapacitor,
} = this.props
return (
<tr className={this.className}>
<td>{this.connectButton}</td>
<td>
<ConnectionLink source={source} currentSource={currentSource} />
<span>{source.url}</span>
</td>
<td className="text-right">
<Authorized requiredRole={EDITOR_ROLE}>
<ConfirmButton
type="btn-danger"
size="btn-xs"
text="Delete Connection"
confirmAction={this.handleDeleteSource}
customClass="delete-source table--show-on-row-hover"
/>
</Authorized>
</td>
<td className="source-table--kapacitor">
<KapacitorDropdown
source={source}
kapacitors={source.kapacitors}
deleteKapacitor={deleteKapacitor}
setActiveKapacitor={setActiveKapacitor}
/>
</td>
</tr>
)
}
private handleDeleteSource = (): void => {
this.props.onDeleteSource(this.props.source)
}
private get connectButton(): ReactElement<HTMLDivElement> {
const {source} = this.props
if (this.isCurrentSource) {
return (
<div className="btn btn-success btn-xs source-table--connect">
Connected
</div>
)
}
return (
<Link
className="btn btn-default btn-xs source-table--connect"
to={`/sources/${source.id}/hosts`}
>
Connect
</Link>
)
}
private get className(): string {
if (this.isCurrentSource) {
return 'hightlight'
}
return ''
}
private get isCurrentSource(): boolean {
const {source, currentSource} = this.props
return source.id === currentSource.id
}
}
export default InfluxTableRow

View File

@ -0,0 +1,118 @@
import React, {PureComponent, ReactElement} from 'react'
import {Link, withRouter, RouteComponentProps} from 'react-router'
import Dropdown from 'src/shared/components/Dropdown'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Source, Kapacitor} from 'src/types'
import {SetActiveKapacitor} from 'src/shared/actions/sources'
interface Props {
source: Source
kapacitors: Kapacitor[]
setActiveKapacitor: SetActiveKapacitor
deleteKapacitor: (Kapacitor: Kapacitor) => void
}
interface KapacitorItem {
text: string
resource: string
kapacitor: Kapacitor
}
class KapacitorDropdown extends PureComponent<
Props & RouteComponentProps<any, any>
> {
public render() {
const {source, router, setActiveKapacitor, deleteKapacitor} = this.props
if (this.isKapacitorsEmpty) {
return (
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/kapacitors/new`}
className="btn btn-xs btn-default"
>
<span className="icon plus" /> Add Kapacitor Connection
</Link>
</Authorized>
)
}
return (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={this.UnauthorizedDropdown}
>
<Dropdown
className="dropdown-260"
buttonColor="btn-primary"
buttonSize="btn-xs"
items={this.kapacitorItems}
onChoose={setActiveKapacitor}
addNew={{
url: `/sources/${source.id}/kapacitors/new`,
text: 'Add Kapacitor Connection',
}}
actions={[
{
icon: 'pencil',
text: 'edit',
handler: item => {
router.push(`${item.resource}/edit`)
},
},
{
icon: 'trash',
text: 'delete',
handler: item => {
deleteKapacitor(item.kapacitor)
},
confirmable: true,
},
]}
selected={this.selected}
/>
</Authorized>
)
}
private get UnauthorizedDropdown(): ReactElement<HTMLDivElement> {
return (
<div className="source-table--kapacitor__view-only">{this.selected}</div>
)
}
private get isKapacitorsEmpty(): boolean {
const {kapacitors} = this.props
return !kapacitors || kapacitors.length === 0
}
private get kapacitorItems(): KapacitorItem[] {
const {kapacitors, source} = this.props
return kapacitors.map(k => {
return {
text: k.name,
resource: `/sources/${source.id}/kapacitors/${k.id}`,
kapacitor: k,
}
})
}
private get activeKapacitor(): Kapacitor {
return this.props.kapacitors.find(k => k.active)
}
private get selected(): string {
let selected = ''
if (this.activeKapacitor) {
selected = this.activeKapacitor.name
} else {
selected = this.kapacitorItems[0].text
}
return selected
}
}
export default withRouter<Props>(KapacitorDropdown)

View File

@ -0,0 +1,119 @@
import React, {PureComponent, ReactElement} from 'react'
import {Link, withRouter, RouteComponentProps} from 'react-router'
import Dropdown from 'src/shared/components/Dropdown'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Source, Service} from 'src/types'
import {SetActiveService} from 'src/shared/actions/services'
interface Props {
source: Source
services: Service[]
setActiveService: SetActiveService
deleteService: (service: Service) => void
}
interface ServiceItem {
text: string
resource: string
service: Service
}
class ServiceDropdown extends PureComponent<
Props & RouteComponentProps<any, any>
> {
public render() {
const {source, router, setActiveService, deleteService} = this.props
if (this.isServicesEmpty) {
return (
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/services/new`}
className="btn btn-xs btn-default"
>
<span className="icon plus" /> Add Service Connection
</Link>
</Authorized>
)
}
return (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={this.UnauthorizedDropdown}
>
<Dropdown
className="dropdown-260"
buttonColor="btn-primary"
buttonSize="btn-xs"
items={this.serviceItems}
onChoose={setActiveService}
addNew={{
url: `/sources/${source.id}/services/new`,
text: 'Add Service Connection',
}}
actions={[
{
icon: 'pencil',
text: 'edit',
handler: item => {
router.push(`${item.resource}/edit`)
},
},
{
icon: 'trash',
text: 'delete',
handler: item => {
deleteService(item.service)
},
confirmable: true,
},
]}
selected={this.selected}
/>
</Authorized>
)
}
private get UnauthorizedDropdown(): ReactElement<HTMLDivElement> {
return (
<div className="source-table--service__view-only">{this.selected}</div>
)
}
private get isServicesEmpty(): boolean {
const {services} = this.props
return !services || services.length === 0
}
private get serviceItems(): ServiceItem[] {
const {services, source} = this.props
return services.map(service => {
return {
text: service.name,
resource: `/sources/${source.id}/services/${service.id}`,
service,
}
})
}
private get activeService(): Service {
return this.props.services.find(s => s.active)
}
private get selected(): string {
let selected = ''
if (this.activeService) {
selected = this.activeService.name
} else {
selected = this.serviceItems[0].text
}
return selected
}
}
export default withRouter<Props>(ServiceDropdown)

View File

@ -1,2 +0,0 @@
export const REQUIRED_ROLE_COPY =
'The minimum Role a user must have<br />in order to access this source.'

View File

@ -0,0 +1,5 @@
export const REQUIRED_ROLE_COPY =
'The minimum Role a user must have<br />in order to access this source.'
export const KAPACITOR_TOOLTIP_COPY =
'<p>Kapacitor Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'

View File

@ -1,41 +1,43 @@
import React, {Component} from 'react' import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import { import * as actions from 'src/shared/actions/sources'
removeAndLoadSources, import {notify as notifyAction} from 'src/shared/actions/notifications'
fetchKapacitorsAsync,
setActiveKapacitorAsync,
deleteKapacitorAsync,
} from 'shared/actions/sources'
import {notify as notifyAction} from 'shared/actions/notifications'
import FancyScrollbar from 'shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import SourceIndicator from 'shared/components/SourceIndicator' import SourceIndicator from 'src/shared/components/SourceIndicator'
import InfluxTable from 'src/sources/components/InfluxTable' import InfluxTable from 'src/sources/components/InfluxTable'
import { import {
notifySourceDeleted, notifySourceDeleted,
notifySourceDeleteFailed, notifySourceDeleteFailed,
} from 'shared/copy/notifications' } from 'src/shared/copy/notifications'
const V_NUMBER = VERSION // eslint-disable-line no-undef import {Source, NotificationFunc} from 'src/types'
interface Props {
source: Source
sources: Source[]
notify: NotificationFunc
deleteKapacitor: actions.DeleteKapacitorAsync
fetchKapacitors: actions.FetchKapacitorsAsync
removeAndLoadSources: actions.RemoveAndLoadSources
setActiveKapacitor: actions.SetActiveKapacitorAsync
}
declare var VERSION: string
@ErrorHandling @ErrorHandling
class ManageSources extends Component { class ManageSources extends PureComponent<Props> {
constructor(props) { public componentDidMount() {
super(props)
}
componentDidMount() {
this.props.sources.forEach(source => { this.props.sources.forEach(source => {
this.props.fetchKapacitors(source) this.props.fetchKapacitors(source)
}) })
} }
componentDidUpdate(prevProps) { public componentDidUpdate(prevProps: Props) {
if (prevProps.sources.length !== this.props.sources.length) { if (prevProps.sources.length !== this.props.sources.length) {
this.props.sources.forEach(source => { this.props.sources.forEach(source => {
this.props.fetchKapacitors(source) this.props.fetchKapacitors(source)
@ -43,22 +45,7 @@ class ManageSources extends Component {
} }
} }
handleDeleteSource = source => () => { public render() {
const {notify} = this.props
try {
this.props.removeAndLoadSources(source)
notify(notifySourceDeleted(source.name))
} catch (e) {
notify(notifySourceDeleteFailed(source.name))
}
}
handleSetActiveKapacitor = ({kapacitor}) => {
this.props.setActiveKapacitor(kapacitor)
}
render() {
const {sources, source, deleteKapacitor} = this.props const {sources, source, deleteKapacitor} = this.props
return ( return (
@ -78,34 +65,31 @@ class ManageSources extends Component {
<InfluxTable <InfluxTable
source={source} source={source}
sources={sources} sources={sources}
handleDeleteKapacitor={deleteKapacitor} deleteKapacitor={deleteKapacitor}
handleDeleteSource={this.handleDeleteSource} onDeleteSource={this.handleDeleteSource}
setActiveKapacitor={this.handleSetActiveKapacitor} setActiveKapacitor={this.handleSetActiveKapacitor}
/> />
<p className="version-number">Chronograf Version: {V_NUMBER}</p> <p className="version-number">Chronograf Version: {VERSION}</p>
</div> </div>
</FancyScrollbar> </FancyScrollbar>
</div> </div>
) )
} }
}
const {array, func, shape, string} = PropTypes private handleDeleteSource = (source: Source) => () => {
const {notify} = this.props
ManageSources.propTypes = { try {
source: shape({ this.props.removeAndLoadSources(source)
id: string.isRequired, notify(notifySourceDeleted(source.name))
links: shape({ } catch (e) {
proxy: string.isRequired, notify(notifySourceDeleteFailed(source.name))
self: string.isRequired, }
}), }
}),
sources: array, private handleSetActiveKapacitor = ({kapacitor}) => {
notify: func.isRequired, this.props.setActiveKapacitor(kapacitor)
removeAndLoadSources: func.isRequired, }
fetchKapacitors: func.isRequired,
setActiveKapacitor: func.isRequired,
deleteKapacitor: func.isRequired,
} }
const mapStateToProps = ({sources}) => ({ const mapStateToProps = ({sources}) => ({
@ -113,10 +97,16 @@ const mapStateToProps = ({sources}) => ({
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
removeAndLoadSources: bindActionCreators(removeAndLoadSources, dispatch), removeAndLoadSources: bindActionCreators(
fetchKapacitors: bindActionCreators(fetchKapacitorsAsync, dispatch), actions.removeAndLoadSources,
setActiveKapacitor: bindActionCreators(setActiveKapacitorAsync, dispatch), dispatch
deleteKapacitor: bindActionCreators(deleteKapacitorAsync, dispatch), ),
fetchKapacitors: bindActionCreators(actions.fetchKapacitorsAsync, dispatch),
setActiveKapacitor: bindActionCreators(
actions.setActiveKapacitorAsync,
dispatch
),
deleteKapacitor: bindActionCreators(actions.deleteKapacitorAsync, dispatch),
notify: bindActionCreators(notifyAction, dispatch), notify: bindActionCreators(notifyAction, dispatch),
}) })

View File

@ -16,6 +16,7 @@ import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
import overlayTechnology from 'src/shared/reducers/overlayTechnology' import overlayTechnology from 'src/shared/reducers/overlayTechnology'
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1' import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
import persistStateEnhancer from './persistStateEnhancer' import persistStateEnhancer from './persistStateEnhancer'
import servicesReducer from 'src/shared/reducers/services'
const rootReducer = combineReducers({ const rootReducer = combineReducers({
...statusReducers, ...statusReducers,
@ -28,6 +29,7 @@ const rootReducer = combineReducers({
overlayTechnology, overlayTechnology,
dashTimeV1, dashTimeV1,
routing: routerReducer, routing: routerReducer,
services: servicesReducer,
}) })
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

View File

@ -4,22 +4,18 @@
*/ */
$threesizer-handle: 30px; $threesizer-handle: 30px;
.threesizer { .threesizer {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
&.dragging .threesizer--division { &.dragging .threesizer--division {
@include no-user-select(); @include no-user-select();
pointer-events: none; pointer-events: none;
} }
&.vertical { &.vertical {
flex-direction: row; flex-direction: row;
} }
&.horizontal { &.horizontal {
flex-direction: column; flex-direction: column;
} }
@ -29,55 +25,45 @@ $threesizer-handle: 30px;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
transition: height 0.25s ease-in-out, width 0.25s ease-in-out; transition: height 0.25s ease-in-out, width 0.25s ease-in-out;
&.dragging { &.dragging {
transition: none; transition: none;
} }
&.vertical { &.vertical {
flex-direction: row; flex-direction: row;
} }
&.horizontal { &.horizontal {
flex-direction: column; flex-direction: column;
} }
} }
/* Draggable Handle With Title */ /* Draggable Handle With Title */
.threesizer--handle { .threesizer--handle {
@include no-user-select(); @include no-user-select();
background-color: $g4-onyx; background-color: $g4-onyx;
transition: background-color 0.25s ease, color 0.25s ease; transition: background-color 0.25s ease, color 0.25s ease;
&.vertical { &.vertical {
border-right: solid 2px $g3-castle; border-right: solid 2px $g3-castle;
&:hover, &:hover,
&.dragging { &.dragging {
cursor: col-resize; cursor: col-resize;
} }
} }
&.horizontal { &.horizontal {
border-bottom: solid 2px $g3-castle; border-bottom: solid 2px $g3-castle;
&:hover, &:hover,
&.dragging { &.dragging {
cursor: row-resize; cursor: row-resize;
} }
} }
&:hover { &:hover {
&.disabled { &.disabled {
cursor: pointer; cursor: pointer;
} }
color: $g16-pearl; color: $g16-pearl;
background-color: $g5-pepper; background-color: $g5-pepper;
} }
&.dragging { &.dragging {
color: $c-laser; color: $c-laser;
background-color: $g5-pepper; background-color: $g5-pepper;
@ -93,10 +79,8 @@ $threesizer-handle: 30px;
color: $g11-sidewalk; color: $g11-sidewalk;
z-index: 1; z-index: 1;
transition: transform 0.25s ease; transition: transform 0.25s ease;
&.vertical { &.vertical {
transform: translate(28px, 14px); transform: translate(28px, 14px);
&.threesizer--collapsed { &.threesizer--collapsed {
transform: translate(0, 3px) rotate(90deg); transform: translate(0, 3px) rotate(90deg);
} }
@ -107,22 +91,17 @@ $threesizer-shadow-size: 9px;
$threesizer-z-index: 2; $threesizer-z-index: 2;
$threesizer-shadow-start: fade-out($g0-obsidian, 0.82); $threesizer-shadow-start: fade-out($g0-obsidian, 0.82);
$threesizer-shadow-stop: fade-out($g0-obsidian, 1); $threesizer-shadow-stop: fade-out($g0-obsidian, 1);
.threesizer--contents { .threesizer--contents {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-wrap: nowrap; flex-wrap: nowrap;
position: relative; position: relative;
&.horizontal { &.horizontal {
flex-direction: row; flex-direction: row;
} }
&.vertical { &.vertical {
flex-direction: column; flex-direction: column;
} } // Bottom Shadow
// Bottom Shadow
&.horizontal:after, &.horizontal:after,
&.vertical:after { &.vertical:after {
content: ''; content: '';
@ -131,13 +110,11 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1);
right: 0; right: 0;
z-index: $threesizer-z-index; z-index: $threesizer-z-index;
} }
&.horizontal:after { &.horizontal:after {
width: 100%; width: 100%;
height: $threesizer-shadow-size; height: $threesizer-shadow-size;
@include gradient-v($threesizer-shadow-stop, $threesizer-shadow-start); @include gradient-v($threesizer-shadow-stop, $threesizer-shadow-start);
} }
&.vertical:after { &.vertical:after {
height: 100%; height: 100%;
width: $threesizer-shadow-size; width: $threesizer-shadow-size;
@ -155,29 +132,177 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1);
// Header // Header
.threesizer--header { .threesizer--header {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 11px;
background-color: $g2-kevlar; background-color: $g2-kevlar;
.horizontal>& {
.horizontal > & {
width: 50px; width: 50px;
border-right: 2px solid $g4-onyx; border-right: 2px solid $g4-onyx;
} }
.vertical>& {
.vertical > & {
height: 50px; height: 50px;
border-bottom: 2px solid $g4-onyx; border-bottom: 2px solid $g4-onyx;
} }
} }
.threesizer--body { .threesizer--body {
.horizontal > &:only-child { .horizontal>&:only-child {
width: 100%; width: 100%;
} }
.vertical>&:only-child {
.vertical > &:only-child {
height: 100%; height: 100%;
} }
.threesizer--header+& {
.threesizer--header + & {
flex: 1 0 0; flex: 1 0 0;
} }
} }
// Division context menus
.threesizer-context--buttons {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.analyze--button {
margin-right: 3px
}
.dash-graph-context--button {
width: 24px;
height: 24px;
border-radius: 3px;
font-size: 12px;
position: relative;
color: $g11-sidewalk;
margin-right: 2px;
transition: color 0.25s ease, background-color 0.25s ease;
&:hover,
&.active {
cursor: pointer;
color: $g20-white;
background-color: $g5-pepper;
}
&:last-child {
margin-right: 0;
}
>.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.active {
position: relative;
z-index: 20;
}
}
.dash-graph-context--menu,
.dash-graph-context--menu.default {
z-index: 3;
position: absolute;
top: calc(100% + 8px);
left: 50%;
background-color: $g6-smoke;
transform: translateX(-50%);
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
&:before {
position: absolute;
content: '';
border: 6px solid transparent;
border-bottom-color: $g6-smoke;
left: 50%;
top: 0;
transform: translate(-50%, -100%);
transition: border-color 0.25s ease;
}
.dash-graph-context--menu-item {
@include no-user-select();
white-space: nowrap;
font-size: 12px;
font-weight: 700;
line-height: 26px;
height: 26px;
padding: 0 10px;
color: $g20-white;
transition: background-color 0.25s ease;
&:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
&:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&:hover {
background-color: $g8-storm;
cursor: pointer;
}
&.disabled,
&.disabled:hover {
cursor: default;
background-color: transparent;
font-style: italic;
color: $g11-sidewalk;
}
}
}
.dash-graph-context--menu.primary {
background-color: $c-ocean;
&:before {
border-bottom-color: $c-ocean;
}
.dash-graph-context--menu-item:hover {
background-color: $c-pool;
}
}
.dash-graph-context--menu.warning {
background-color: $c-star;
&:before {
border-bottom-color: $c-star;
}
.dash-graph-context--menu-item:hover {
background-color: $c-comet;
}
}
.dash-graph-context--menu.success {
background-color: $c-rainforest;
&:before {
border-bottom-color: $c-rainforest;
}
.dash-graph-context--menu-item:hover {
background-color: $c-honeydew;
}
}
.dash-graph-context--menu.danger {
background-color: $c-curacao;
&:before {
border-bottom-color: $c-curacao;
}
.dash-graph-context--menu-item:hover {
background-color: $c-dreamsicle;
}
}
// Header Dropdown Menu
.threesizer--menu {
.dropdown-menu {
right: 0;
}
}
// Hide Header children when collapsed
.threesizer--handle.vertical.threesizer--collapsed+.threesizer--contents>.threesizer--header>* {
display: none;
}

View File

@ -19,3 +19,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.error-warning {
color: $c-dreamsicle;
cursor: pointer;
}

View File

@ -0,0 +1,4 @@
.ifql-overlay {
max-width: 500px;
margin: 0 auto;
}

View File

@ -3,6 +3,7 @@
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
*/ */
@import '../components/time-machine/ifql-overlay';
@import '../components/time-machine/ifql-editor'; @import '../components/time-machine/ifql-editor';
@import '../components/time-machine/ifql-builder'; @import '../components/time-machine/ifql-builder';
@import '../components/time-machine/ifql-explorer'; @import '../components/time-machine/ifql-explorer';

View File

@ -10,6 +10,11 @@ export type OnGenerateScript = (script: string) => void
export type OnChangeScript = (script: string) => void export type OnChangeScript = (script: string) => void
export type OnSubmitScript = () => void export type OnSubmitScript = () => void
export interface Status {
type: string
text: string
}
export interface Handlers { export interface Handlers {
onAddNode: OnAddNode onAddNode: OnAddNode
onChangeArg: OnChangeArg onChangeArg: OnChangeArg

View File

@ -1,3 +1,4 @@
import {Service, NewService} from './services'
import {AuthLinks, Organization, Role, User, Me} from './auth' import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard' import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
import { import {
@ -53,4 +54,6 @@ export {
Notification, Notification,
NotificationFunc, NotificationFunc,
Axes, Axes,
Service,
NewService,
} }

View File

@ -6,4 +6,4 @@ export interface Notification {
message: string message: string
} }
export type NotificationFunc = () => Notification export type NotificationFunc = (message: any) => Notification

25
ui/src/types/services.ts Normal file
View File

@ -0,0 +1,25 @@
export interface NewService {
url: string
name: string
type: string
username?: string
password?: string
active: boolean
insecureSkipVerify: boolean
}
export interface Service {
id?: string
url: string
name: string
type: string
username?: string
password?: string
active: boolean
insecureSkipVerify: boolean
links: {
source: string
self: string
proxy: string
}
}

View File

@ -1,4 +1,4 @@
import {Kapacitor} from './' import {Kapacitor, Service} from './'
export interface Source { export interface Source {
id: string id: string
@ -17,6 +17,7 @@ export interface Source {
defaultRP: string defaultRP: string
links: SourceLinks links: SourceLinks
kapacitors?: Kapacitor[] // this field does not exist on the server type for Source and is added in the client in the reducer for loading kapacitors. kapacitors?: Kapacitor[] // this field does not exist on the server type for Source and is added in the client in the reducer for loading kapacitors.
services?: Service[]
} }
export interface SourceLinks { export interface SourceLinks {
@ -31,4 +32,5 @@ export interface SourceLinks {
databases: string databases: string
annotations: string annotations: string
health: string health: string
services: string
} }

View File

View File

@ -18,6 +18,7 @@ import {ColorString, ColorNumber} from 'src/types/colors'
import {CellType} from 'src/types/dashboard' import {CellType} from 'src/types/dashboard'
export const sourceLinks: SourceLinks = { export const sourceLinks: SourceLinks = {
services: '/chronograf/v1/sources/4',
self: '/chronograf/v1/sources/4', self: '/chronograf/v1/sources/4',
kapacitors: '/chronograf/v1/sources/4/kapacitors', kapacitors: '/chronograf/v1/sources/4/kapacitors',
proxy: '/chronograf/v1/sources/4/proxy', proxy: '/chronograf/v1/sources/4/proxy',

View File

@ -215,3 +215,133 @@ export const ArrowFunction = {
}, },
], ],
} }
export const Fork = {
type: 'Program',
location: {
start: {line: 1, column: 1},
end: {line: 1, column: 42},
source: 'tele = from(db: "telegraf")\ntele |\u003e sum()',
},
body: [
{
type: 'VariableDeclaration',
location: {
start: {line: 1, column: 1},
end: {line: 1, column: 28},
source: 'tele = from(db: "telegraf")',
},
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
location: {
start: {line: 1, column: 1},
end: {line: 1, column: 5},
source: 'tele',
},
name: 'tele',
},
init: {
type: 'CallExpression',
location: {
start: {line: 1, column: 8},
end: {line: 1, column: 28},
source: 'from(db: "telegraf")',
},
callee: {
type: 'Identifier',
location: {
start: {line: 1, column: 8},
end: {line: 1, column: 12},
source: 'from',
},
name: 'from',
},
arguments: [
{
type: 'ObjectExpression',
location: {
start: {line: 1, column: 13},
end: {line: 1, column: 27},
source: 'db: "telegraf"',
},
properties: [
{
type: 'Property',
location: {
start: {line: 1, column: 13},
end: {line: 1, column: 27},
source: 'db: "telegraf"',
},
key: {
type: 'Identifier',
location: {
start: {line: 1, column: 13},
end: {line: 1, column: 15},
source: 'db',
},
name: 'db',
},
value: {
type: 'StringLiteral',
location: {
start: {line: 1, column: 17},
end: {line: 1, column: 27},
source: '"telegraf"',
},
value: 'telegraf',
},
},
],
},
],
},
},
],
},
{
type: 'ExpressionStatement',
location: {
start: {line: 2, column: 1},
end: {line: 2, column: 14},
source: 'tele |\u003e sum()',
},
expression: {
type: 'PipeExpression',
location: {
start: {line: 2, column: 6},
end: {line: 2, column: 14},
source: '|\u003e sum()',
},
argument: {
type: 'Identifier',
location: {
start: {line: 2, column: 1},
end: {line: 2, column: 5},
source: 'tele',
},
name: 'tele',
},
call: {
type: 'CallExpression',
location: {
start: {line: 2, column: 9},
end: {line: 2, column: 14},
source: 'sum()',
},
callee: {
type: 'Identifier',
location: {
start: {line: 2, column: 9},
end: {line: 2, column: 12},
source: 'sum',
},
name: 'sum',
},
},
},
},
],
}

View File

@ -1,7 +1,12 @@
import Walker from 'src/ifql/ast/walker' import Walker from 'src/ifql/ast/walker'
import From from 'test/ifql/ast/from' import From from 'test/ifql/ast/from'
import Complex from 'test/ifql/ast/complex' import Complex from 'test/ifql/ast/complex'
import {StringLiteral, Expression, ArrowFunction} from 'test/ifql/ast/variable' import {
StringLiteral,
Expression,
ArrowFunction,
Fork,
} from 'test/ifql/ast/variable'
describe('IFQL.AST.Walker', () => { describe('IFQL.AST.Walker', () => {
describe('Walker#functions', () => { describe('Walker#functions', () => {
@ -80,7 +85,7 @@ describe('IFQL.AST.Walker', () => {
}) })
}) })
describe.only('a single ArrowFunction variable', () => { describe('a single ArrowFunction variable', () => {
it('returns the expected list', () => { it('returns the expected list', () => {
const walker = new Walker(ArrowFunction) const walker = new Walker(ArrowFunction)
expect(walker.body).toEqual([ expect(walker.body).toEqual([
@ -104,6 +109,45 @@ describe('IFQL.AST.Walker', () => {
]) ])
}) })
}) })
describe('forking', () => {
it('return the expected list of objects', () => {
const walker = new Walker(Fork)
expect(walker.body).toEqual([
{
type: 'VariableDeclaration',
source: 'tele = from(db: "telegraf")',
declarations: [
{
name: 'tele',
type: 'CallExpression',
source: 'tele = from(db: "telegraf")',
funcs: [
{
name: 'from',
source: 'from(db: "telegraf")',
args: [
{
key: 'db',
value: 'telegraf',
},
],
},
],
},
],
},
{
type: 'PipeExpression',
source: 'tele |> sum()',
funcs: [
{args: [], name: 'tele', source: 'tele'},
{args: [], name: 'sum', source: '|> sum()'},
],
},
])
})
})
}) })
}) })
}) })

View File

@ -6,9 +6,14 @@ const setup = () => {
const props = { const props = {
script: '', script: '',
body: [], body: [],
data: '',
status: {type: '', text: ''},
suggestions: [], suggestions: [],
onSubmitScript: () => {}, onSubmitScript: () => {},
onChangeScript: () => {}, onChangeScript: () => {},
onAnalyze: () => {},
onAppendFrom: () => {},
onAppendJoin: () => {},
} }
const wrapper = shallow(<TimeMachine {...props} />) const wrapper = shallow(<TimeMachine {...props} />)

View File

@ -13,6 +13,9 @@ const setup = () => {
suggestions: '', suggestions: '',
ast: '', ast: '',
}, },
services: [],
sources: [],
notify: () => {},
} }
const wrapper = shallow(<IFQLPage {...props} />) const wrapper = shallow(<IFQLPage {...props} />)

View File

@ -21,6 +21,7 @@ export const me = {
} }
export const sourceLinks: SourceLinks = { export const sourceLinks: SourceLinks = {
services: '/chronograf/v1/sources/16/services',
self: '/chronograf/v1/sources/16', self: '/chronograf/v1/sources/16',
kapacitors: '/chronograf/v1/sources/16/kapacitors', kapacitors: '/chronograf/v1/sources/16/kapacitors',
proxy: '/chronograf/v1/sources/16/proxy', proxy: '/chronograf/v1/sources/16/proxy',
@ -93,6 +94,22 @@ export const kapacitor = {
}, },
} }
export const service = {
id: '1',
url: 'localhost:8082',
type: 'ifql',
name: 'IFQL',
username: '',
password: '',
active: false,
insecureSkipVerify: false,
links: {
source: '/chronograf/v1/sources/1',
proxy: '/chronograf/v1/sources/1/services/2/proxy',
self: '/chronograf/v1/sources/1/services/2',
},
}
export const kapacitorRules = [ export const kapacitorRules = [
{ {
id: '1', id: '1',

View File

@ -0,0 +1,60 @@
import reducer, {initialState} from 'src/shared/reducers/services'
import {
addService,
loadServices,
deleteService,
updateService,
} from 'src/shared/actions/services'
import {Service} from 'src/types'
import {service} from 'test/resources'
const services = (): Service[] => {
return reducer(initialState, addService(service))
}
describe('Shared.Reducers.services', () => {
describe('LOAD_SERVICES', () => {
it('correctly loads services', () => {
const s1 = {...service, id: '1'}
const s2 = {...service, id: '2'}
const expected = [s1, s2]
const actual = reducer(initialState, loadServices(expected))
expect(actual).toEqual(expected)
})
})
describe('ADD_SERVICE', () => {
it('can add a service', () => {
const expected = service
const [actual] = reducer(initialState, addService(service))
expect(actual).toEqual(expected)
})
})
describe('DELETE_SERVICE', () => {
it('can delete a service', () => {
const state = services()
const actual = reducer(state, deleteService(service))
expect(actual).toEqual([])
})
})
describe('UPDATE_SERVICE', () => {
it('can update a service', () => {
const name = 'updated name'
const type = 'updated type'
const expected = {...service, name, type}
const state = services()
const [actual] = reducer(state, updateService(expected))
expect(actual).toEqual(expected)
})
})
})

View File

@ -1,58 +0,0 @@
import reducer from 'shared/reducers/sources'
import {updateSource, addSource} from 'shared/actions/sources'
describe('Shared.Reducers.sources', () => {
it('can correctly show default sources when adding a source', () => {
let state = []
state = reducer(
state,
addSource({
id: '1',
default: true,
})
)
state = reducer(
state,
addSource({
id: '2',
default: true,
})
)
expect(state.filter(s => s.default).length).toBe(1)
})
it('can correctly show default sources when updating a source', () => {
let state = []
state = reducer(
state,
addSource({
id: '1',
default: true,
})
)
state = reducer(
state,
addSource({
id: '2',
default: true,
})
)
state = reducer(
state,
updateSource({
id: '1',
default: true,
})
)
expect(state.find(({id}) => id === '1').default).toBe(true)
expect(state.find(({id}) => id === '2').default).toBe(false)
})
})

View File

@ -0,0 +1,74 @@
import reducer, {initialState} from 'src/shared/reducers/sources'
import {updateSource, addSource, loadSources} from 'src/shared/actions/sources'
import {source} from 'test/resources'
describe('Shared.Reducers.sources', () => {
it('can LOAD_SOURCES', () => {
const expected = [{...source, id: '1'}]
const actual = reducer(initialState, loadSources(expected))
expect(actual).toEqual(expected)
})
describe('ADD_SOURCES', () => {
it('can ADD_SOURCES', () => {
let state = []
state = reducer(
state,
addSource({
...source,
id: '1',
default: true,
})
)
state = reducer(
state,
addSource({
...source,
id: '2',
default: true,
})
)
expect(state.filter(s => s.default).length).toBe(1)
})
it('can correctly show default sources when updating a source', () => {
let state = []
state = reducer(
initialState,
addSource({
...source,
id: '1',
default: true,
})
)
state = reducer(
state,
addSource({
...source,
id: '2',
default: true,
})
)
state = reducer(
state,
updateSource({
...source,
id: '1',
default: true,
})
)
expect(state.find(({id}) => id === '1').default).toBe(true)
expect(state.find(({id}) => id === '2').default).toBe(false)
})
})
})