Merge branch 'master' into refresh-list-after-metas

pull/2327/head
Andrew Watkins 2017-11-27 17:17:07 -05:00 committed by GitHub
commit 8caf145e97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 2333 additions and 1217 deletions

View File

@ -10,8 +10,13 @@
1. [#2292](https://github.com/influxdata/chronograf/pull/2292): Source extra command line options from defaults file 1. [#2292](https://github.com/influxdata/chronograf/pull/2292): Source extra command line options from defaults file
1. [#2327](https://github.com/influxdata/chronograf/pull/2327): After CREATE/DELETE queries, refresh list of databases in Data Explorer 1. [#2327](https://github.com/influxdata/chronograf/pull/2327): After CREATE/DELETE queries, refresh list of databases in Data Explorer
1. [#2327](https://github.com/influxdata/chronograf/pull/2327): Visualize CREATE/DELETE queries with Table view in Data Explorer 1. [#2327](https://github.com/influxdata/chronograf/pull/2327): Visualize CREATE/DELETE queries with Table view in Data Explorer
1. [#2329](https://github.com/influxdata/chronograf/pull/2329): Include tag values alongside measurement name in Data Explorer result tabs
1. [#2386](https://github.com/influxdata/chronograf/pull/2386): Fix queries that include regex, numbers and wildcard
1. [#2408](https://github.com/influxdata/chronograf/pull/2408): Fix updated Dashboard names not updating dashboard list
### Features ### Features
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
### UI Improvements ### UI Improvements
## v1.3.10.0 [2017-10-24] ## v1.3.10.0 [2017-10-24]

View File

@ -23,42 +23,14 @@ ${BINARY}: $(SOURCES) .bindata .jsdep .godep
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
define CHRONOGIRAFFE define CHRONOGIRAFFE
tLf iCf. ._ o o
.CCC. tCC: \_`-)|_
CGG; CGG: ,"" _\_
tG0Gt: GGGGGGGGGGGGGGGG1 .,:, ," ## | 0 0.
LG1,,:1CC: .GGL;iLC1iii1LCi;GG1 .1GCL1iGG1 ," ## ,-\__ `.
LG1:::;i1CGGt;;;;;;L0t;;;;;;GGGC1;;::,iGC ," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!"
,ii:. 1GG1iiii;;tfiC;;;;;;;GGCfCGCGGC, ," ## /
fGCiiiiGi1Lt;;iCLL,i;;;CGt ," ## /
fGG11iiii1C1iiiiiGt1;;;;;CGf
.GGLLL1i1CitfiiL1iCi;;iLCGGt
.CGL11LGCCCCCCCLLCGG1;1GG;
CGL1tf1111iiiiiiL1ifGG,
LGCff1fCt1tCfiiCiCGC
LGGf111111111iCGGt
fGGGGGGGGGGGGGGi
ifii111111itL
;f1i11111iitf
;f1iiiiiii1tf
:fi111iii11tf
:fi111ii1i1tf
:f111111ii1tt
,L111111ii1tt
.Li1111i1111CCCCCCCCCCCCCCLt;
L111ii11111ittttt1tttttittti1fC;
f1111ii111i1ttttt1;iii1ittt1ttttCt.
tt11ii111tti1ttt1tt1;11;;;;iitttifCCCL,
11i1i11ttttti;1t1;;;ttt1;;ii;itti;L,;CCL
;f;;;;1tttti;;ttti;;;;;;;;;;;1tt1ifi .CCi
,L;itti;;;it;;;;;tt1;;;t1;;;;;;ii;t; :CC,
L;;;;iti;;;;;;;;;;;;;;;;;;;;;;;i;L, ;CC.
ti;;;iLLfffi;;;;;ittt11i;;;;;;;;;L tCCfff;
it;;;;;;L,ti;;;;;1Ltttft1t;;;;;;1t ;CCCL;
:f;;;;;;L.ti;;;;;tftttf1,f;;;;;;f: ;CC1:
.L;;;;;;L.t1;;;;;tt111fi,f;;;;;;L.
1Li;;iL1 :Ci;;;tL1i1fC, Lt;;;;Li
.;tt; ifLt:;fLf; ;LCCt,
endef endef
export CHRONOGIRAFFE export CHRONOGIRAFFE
chronogiraffe: ${BINARY} chronogiraffe: ${BINARY}

View File

@ -86,6 +86,7 @@ func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (ch
id, _ := b.NextSequence() id, _ := b.NextSequence()
src.ID = chronograf.DashboardID(id) src.ID = chronograf.DashboardID(id)
// TODO: use FormatInt
strID := strconv.Itoa(int(id)) strID := strconv.Itoa(int(id))
for i, cell := range src.Cells { for i, cell := range src.Cells {
cid, err := d.IDs.Generate() cid, err := d.IDs.Generate()
@ -95,12 +96,11 @@ func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (ch
cell.ID = cid cell.ID = cid
src.Cells[i] = cell src.Cells[i] = cell
} }
if v, err := internal.MarshalDashboard(src); err != nil { v, err := internal.MarshalDashboard(src)
return err if err != nil {
} else if err := b.Put([]byte(strID), v); err != nil {
return err return err
} }
return nil return b.Put([]byte(strID), v)
}); err != nil { }); err != nil {
return chronograf.Dashboard{}, err return chronograf.Dashboard{}, err
} }

View File

@ -191,12 +191,26 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
if q.Range != nil { if q.Range != nil {
r.Upper, r.Lower = q.Range.Upper, q.Range.Lower r.Upper, r.Lower = q.Range.Upper, q.Range.Lower
} }
q.Shifts = q.QueryConfig.Shifts
queries[j] = &Query{ queries[j] = &Query{
Command: q.Command, Command: q.Command,
Label: q.Label, Label: q.Label,
Range: r, Range: r,
Source: q.Source, Source: q.Source,
} }
shifts := make([]*TimeShift, len(q.Shifts))
for k := range q.Shifts {
shift := &TimeShift{
Label: q.Shifts[k].Label,
Unit: q.Shifts[k].Unit,
Quantity: q.Shifts[k].Quantity,
}
shifts[k] = shift
}
queries[j].Shifts = shifts
} }
axes := make(map[string]*Axis, len(c.Axes)) axes := make(map[string]*Axis, len(c.Axes))
@ -277,12 +291,26 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
Label: q.Label, Label: q.Label,
Source: q.Source, Source: q.Source,
} }
if q.Range.Upper != q.Range.Lower { if q.Range.Upper != q.Range.Lower {
queries[j].Range = &chronograf.Range{ queries[j].Range = &chronograf.Range{
Upper: q.Range.Upper, Upper: q.Range.Upper,
Lower: q.Range.Lower, Lower: q.Range.Lower,
} }
} }
shifts := make([]chronograf.TimeShift, len(q.Shifts))
for k := range q.Shifts {
shift := chronograf.TimeShift{
Label: q.Shifts[k].Label,
Unit: q.Shifts[k].Unit,
Quantity: q.Shifts[k].Quantity,
}
shifts[k] = shift
}
queries[j].Shifts = shifts
} }
axes := make(map[string]chronograf.Axis, len(c.Axes)) axes := make(map[string]chronograf.Axis, len(c.Axes))
@ -330,9 +358,9 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
templates := make([]chronograf.Template, len(pb.Templates)) templates := make([]chronograf.Template, len(pb.Templates))
for i, t := range pb.Templates { for i, t := range pb.Templates {
vals := make([]chronograf.BasicTemplateValue, len(t.Values)) vals := make([]chronograf.TemplateValue, len(t.Values))
for j, v := range t.Values { for j, v := range t.Values {
vals[j] = chronograf.BasicTemplateValue{ vals[j] = chronograf.TemplateValue{
Selected: v.Selected, Selected: v.Selected,
Type: v.Type, Type: v.Type,
Value: v.Value, Value: v.Value,
@ -341,7 +369,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
template := chronograf.Template{ template := chronograf.Template{
ID: chronograf.TemplateID(t.ID), ID: chronograf.TemplateID(t.ID),
BasicTemplateVar: chronograf.BasicTemplateVar{ TemplateVar: chronograf.TemplateVar{
Var: t.TempVar, Var: t.TempVar,
Values: vals, Values: vals,
}, },
@ -434,8 +462,5 @@ func UnmarshalUser(data []byte, u *chronograf.User) error {
// UnmarshalUserPB decodes a user from binary protobuf data. // UnmarshalUserPB decodes a user from binary protobuf data.
// We are ignoring the password for now. // We are ignoring the password for now.
func UnmarshalUserPB(data []byte, u *User) error { func UnmarshalUserPB(data []byte, u *User) error {
if err := proto.Unmarshal(data, u); err != nil { return proto.Unmarshal(data, u)
return err
}
return nil
} }

View File

@ -20,6 +20,7 @@ It has these top-level messages:
Layout Layout
Cell Cell
Query Query
TimeShift
Range Range
AlertRule AlertRule
User User
@ -60,6 +61,83 @@ func (m *Source) String() string { return proto.CompactTextString(m)
func (*Source) ProtoMessage() {} func (*Source) ProtoMessage() {}
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} } func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
func (m *Source) GetID() int64 {
if m != nil {
return m.ID
}
return 0
}
func (m *Source) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Source) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Source) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *Source) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
func (m *Source) GetURL() string {
if m != nil {
return m.URL
}
return ""
}
func (m *Source) GetDefault() bool {
if m != nil {
return m.Default
}
return false
}
func (m *Source) GetTelegraf() string {
if m != nil {
return m.Telegraf
}
return ""
}
func (m *Source) GetInsecureSkipVerify() bool {
if m != nil {
return m.InsecureSkipVerify
}
return false
}
func (m *Source) GetMetaURL() string {
if m != nil {
return m.MetaURL
}
return ""
}
func (m *Source) GetSharedSecret() string {
if m != nil {
return m.SharedSecret
}
return ""
}
type Dashboard struct { type Dashboard struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
@ -72,6 +150,20 @@ func (m *Dashboard) String() string { return proto.CompactTextString(
func (*Dashboard) ProtoMessage() {} func (*Dashboard) ProtoMessage() {}
func (*Dashboard) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{1} } func (*Dashboard) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{1} }
func (m *Dashboard) GetID() int64 {
if m != nil {
return m.ID
}
return 0
}
func (m *Dashboard) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Dashboard) GetCells() []*DashboardCell { func (m *Dashboard) GetCells() []*DashboardCell {
if m != nil { if m != nil {
return m.Cells return m.Cells
@ -103,6 +195,34 @@ func (m *DashboardCell) String() string { return proto.CompactTextStr
func (*DashboardCell) ProtoMessage() {} func (*DashboardCell) ProtoMessage() {}
func (*DashboardCell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} } func (*DashboardCell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} }
func (m *DashboardCell) GetX() int32 {
if m != nil {
return m.X
}
return 0
}
func (m *DashboardCell) GetY() int32 {
if m != nil {
return m.Y
}
return 0
}
func (m *DashboardCell) GetW() int32 {
if m != nil {
return m.W
}
return 0
}
func (m *DashboardCell) GetH() int32 {
if m != nil {
return m.H
}
return 0
}
func (m *DashboardCell) GetQueries() []*Query { func (m *DashboardCell) GetQueries() []*Query {
if m != nil { if m != nil {
return m.Queries return m.Queries
@ -110,6 +230,27 @@ func (m *DashboardCell) GetQueries() []*Query {
return nil return nil
} }
func (m *DashboardCell) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *DashboardCell) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *DashboardCell) GetID() string {
if m != nil {
return m.ID
}
return ""
}
func (m *DashboardCell) GetAxes() map[string]*Axis { func (m *DashboardCell) GetAxes() map[string]*Axis {
if m != nil { if m != nil {
return m.Axes return m.Axes
@ -118,7 +259,7 @@ func (m *DashboardCell) GetAxes() map[string]*Axis {
} }
type Axis struct { type Axis struct {
LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"` LegacyBounds []int64 `protobuf:"varint,1,rep,packed,name=legacyBounds" json:"legacyBounds,omitempty"`
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"` Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"`
Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"`
@ -132,6 +273,55 @@ func (m *Axis) String() string { return proto.CompactTextString(m) }
func (*Axis) ProtoMessage() {} func (*Axis) ProtoMessage() {}
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} } func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
func (m *Axis) GetLegacyBounds() []int64 {
if m != nil {
return m.LegacyBounds
}
return nil
}
func (m *Axis) GetBounds() []string {
if m != nil {
return m.Bounds
}
return nil
}
func (m *Axis) GetLabel() string {
if m != nil {
return m.Label
}
return ""
}
func (m *Axis) GetPrefix() string {
if m != nil {
return m.Prefix
}
return ""
}
func (m *Axis) GetSuffix() string {
if m != nil {
return m.Suffix
}
return ""
}
func (m *Axis) GetBase() string {
if m != nil {
return m.Base
}
return ""
}
func (m *Axis) GetScale() string {
if m != nil {
return m.Scale
}
return ""
}
type Template struct { type Template 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"`
TempVar string `protobuf:"bytes,2,opt,name=temp_var,json=tempVar,proto3" json:"temp_var,omitempty"` TempVar string `protobuf:"bytes,2,opt,name=temp_var,json=tempVar,proto3" json:"temp_var,omitempty"`
@ -146,6 +336,20 @@ func (m *Template) String() string { return proto.CompactTextString(m
func (*Template) ProtoMessage() {} func (*Template) ProtoMessage() {}
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
func (m *Template) GetID() string {
if m != nil {
return m.ID
}
return ""
}
func (m *Template) GetTempVar() string {
if m != nil {
return m.TempVar
}
return ""
}
func (m *Template) GetValues() []*TemplateValue { func (m *Template) GetValues() []*TemplateValue {
if m != nil { if m != nil {
return m.Values return m.Values
@ -153,6 +357,20 @@ func (m *Template) GetValues() []*TemplateValue {
return nil return nil
} }
func (m *Template) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Template) GetLabel() string {
if m != nil {
return m.Label
}
return ""
}
func (m *Template) GetQuery() *TemplateQuery { func (m *Template) GetQuery() *TemplateQuery {
if m != nil { if m != nil {
return m.Query return m.Query
@ -171,6 +389,27 @@ func (m *TemplateValue) String() string { return proto.CompactTextStr
func (*TemplateValue) ProtoMessage() {} func (*TemplateValue) ProtoMessage() {}
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
func (m *TemplateValue) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *TemplateValue) GetValue() string {
if m != nil {
return m.Value
}
return ""
}
func (m *TemplateValue) GetSelected() bool {
if m != nil {
return m.Selected
}
return false
}
type TemplateQuery struct { type TemplateQuery struct {
Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
Db string `protobuf:"bytes,2,opt,name=db,proto3" json:"db,omitempty"` Db string `protobuf:"bytes,2,opt,name=db,proto3" json:"db,omitempty"`
@ -185,6 +424,48 @@ func (m *TemplateQuery) String() string { return proto.CompactTextStr
func (*TemplateQuery) ProtoMessage() {} func (*TemplateQuery) ProtoMessage() {}
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
func (m *TemplateQuery) GetCommand() string {
if m != nil {
return m.Command
}
return ""
}
func (m *TemplateQuery) GetDb() string {
if m != nil {
return m.Db
}
return ""
}
func (m *TemplateQuery) GetRp() string {
if m != nil {
return m.Rp
}
return ""
}
func (m *TemplateQuery) GetMeasurement() string {
if m != nil {
return m.Measurement
}
return ""
}
func (m *TemplateQuery) GetTagKey() string {
if m != nil {
return m.TagKey
}
return ""
}
func (m *TemplateQuery) GetFieldKey() string {
if m != nil {
return m.FieldKey
}
return ""
}
type Server struct { type Server struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
@ -200,6 +481,55 @@ func (m *Server) String() string { return proto.CompactTextString(m)
func (*Server) ProtoMessage() {} func (*Server) ProtoMessage() {}
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func (m *Server) GetID() int64 {
if m != nil {
return m.ID
}
return 0
}
func (m *Server) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Server) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *Server) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
func (m *Server) GetURL() string {
if m != nil {
return m.URL
}
return ""
}
func (m *Server) GetSrcID() int64 {
if m != nil {
return m.SrcID
}
return 0
}
func (m *Server) GetActive() bool {
if m != nil {
return m.Active
}
return false
}
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"`
@ -213,6 +543,27 @@ func (m *Layout) String() string { return proto.CompactTextString(m)
func (*Layout) ProtoMessage() {} func (*Layout) ProtoMessage() {}
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
func (m *Layout) GetID() string {
if m != nil {
return m.ID
}
return ""
}
func (m *Layout) GetApplication() string {
if m != nil {
return m.Application
}
return ""
}
func (m *Layout) GetMeasurement() string {
if m != nil {
return m.Measurement
}
return ""
}
func (m *Layout) GetCells() []*Cell { func (m *Layout) GetCells() []*Cell {
if m != nil { if m != nil {
return m.Cells return m.Cells
@ -220,6 +571,13 @@ func (m *Layout) GetCells() []*Cell {
return nil return nil
} }
func (m *Layout) GetAutoflow() bool {
if m != nil {
return m.Autoflow
}
return false
}
type Cell struct { type Cell struct {
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"` X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"` Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
@ -228,7 +586,7 @@ type Cell struct {
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"` Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
I string `protobuf:"bytes,6,opt,name=i,proto3" json:"i,omitempty"` I string `protobuf:"bytes,6,opt,name=i,proto3" json:"i,omitempty"`
Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"`
Yranges []int64 `protobuf:"varint,8,rep,name=yranges" json:"yranges,omitempty"` Yranges []int64 `protobuf:"varint,8,rep,packed,name=yranges" json:"yranges,omitempty"`
Ylabels []string `protobuf:"bytes,9,rep,name=ylabels" json:"ylabels,omitempty"` Ylabels []string `protobuf:"bytes,9,rep,name=ylabels" json:"ylabels,omitempty"`
Type string `protobuf:"bytes,10,opt,name=type,proto3" json:"type,omitempty"` Type string `protobuf:"bytes,10,opt,name=type,proto3" json:"type,omitempty"`
Axes map[string]*Axis `protobuf:"bytes,11,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"` Axes map[string]*Axis `protobuf:"bytes,11,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
@ -239,6 +597,34 @@ func (m *Cell) String() string { return proto.CompactTextString(m) }
func (*Cell) ProtoMessage() {} func (*Cell) ProtoMessage() {}
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} } func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
func (m *Cell) GetX() int32 {
if m != nil {
return m.X
}
return 0
}
func (m *Cell) GetY() int32 {
if m != nil {
return m.Y
}
return 0
}
func (m *Cell) GetW() int32 {
if m != nil {
return m.W
}
return 0
}
func (m *Cell) GetH() int32 {
if m != nil {
return m.H
}
return 0
}
func (m *Cell) GetQueries() []*Query { func (m *Cell) GetQueries() []*Query {
if m != nil { if m != nil {
return m.Queries return m.Queries
@ -246,6 +632,41 @@ func (m *Cell) GetQueries() []*Query {
return nil return nil
} }
func (m *Cell) GetI() string {
if m != nil {
return m.I
}
return ""
}
func (m *Cell) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Cell) GetYranges() []int64 {
if m != nil {
return m.Yranges
}
return nil
}
func (m *Cell) GetYlabels() []string {
if m != nil {
return m.Ylabels
}
return nil
}
func (m *Cell) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Cell) GetAxes() map[string]*Axis { func (m *Cell) GetAxes() map[string]*Axis {
if m != nil { if m != nil {
return m.Axes return m.Axes
@ -254,14 +675,15 @@ func (m *Cell) GetAxes() map[string]*Axis {
} }
type Query struct { type Query struct {
Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"` Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"`
DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"` DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"`
RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"` RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"`
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"` GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"`
Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"` Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"`
Label string `protobuf:"bytes,6,opt,name=Label,proto3" json:"Label,omitempty"` Label string `protobuf:"bytes,6,opt,name=Label,proto3" json:"Label,omitempty"`
Range *Range `protobuf:"bytes,7,opt,name=Range" json:"Range,omitempty"` Range *Range `protobuf:"bytes,7,opt,name=Range" json:"Range,omitempty"`
Source string `protobuf:"bytes,8,opt,name=Source,proto3" json:"Source,omitempty"` Source string `protobuf:"bytes,8,opt,name=Source,proto3" json:"Source,omitempty"`
Shifts []*TimeShift `protobuf:"bytes,9,rep,name=Shifts" json:"Shifts,omitempty"`
} }
func (m *Query) Reset() { *m = Query{} } func (m *Query) Reset() { *m = Query{} }
@ -269,6 +691,48 @@ func (m *Query) String() string { return proto.CompactTextString(m) }
func (*Query) ProtoMessage() {} func (*Query) ProtoMessage() {}
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} } func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
func (m *Query) GetCommand() string {
if m != nil {
return m.Command
}
return ""
}
func (m *Query) GetDB() string {
if m != nil {
return m.DB
}
return ""
}
func (m *Query) GetRP() string {
if m != nil {
return m.RP
}
return ""
}
func (m *Query) GetGroupBys() []string {
if m != nil {
return m.GroupBys
}
return nil
}
func (m *Query) GetWheres() []string {
if m != nil {
return m.Wheres
}
return nil
}
func (m *Query) GetLabel() string {
if m != nil {
return m.Label
}
return ""
}
func (m *Query) GetRange() *Range { func (m *Query) GetRange() *Range {
if m != nil { if m != nil {
return m.Range return m.Range
@ -276,6 +740,52 @@ func (m *Query) GetRange() *Range {
return nil return nil
} }
func (m *Query) GetSource() string {
if m != nil {
return m.Source
}
return ""
}
func (m *Query) GetShifts() []*TimeShift {
if m != nil {
return m.Shifts
}
return nil
}
type TimeShift struct {
Label string `protobuf:"bytes,1,opt,name=Label,proto3" json:"Label,omitempty"`
Unit string `protobuf:"bytes,2,opt,name=Unit,proto3" json:"Unit,omitempty"`
Quantity string `protobuf:"bytes,3,opt,name=Quantity,proto3" json:"Quantity,omitempty"`
}
func (m *TimeShift) Reset() { *m = TimeShift{} }
func (m *TimeShift) String() string { return proto.CompactTextString(m) }
func (*TimeShift) ProtoMessage() {}
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
func (m *TimeShift) GetLabel() string {
if m != nil {
return m.Label
}
return ""
}
func (m *TimeShift) GetUnit() string {
if m != nil {
return m.Unit
}
return ""
}
func (m *TimeShift) GetQuantity() string {
if m != nil {
return m.Quantity
}
return ""
}
type Range struct { type Range struct {
Upper int64 `protobuf:"varint,1,opt,name=Upper,proto3" json:"Upper,omitempty"` Upper int64 `protobuf:"varint,1,opt,name=Upper,proto3" json:"Upper,omitempty"`
Lower int64 `protobuf:"varint,2,opt,name=Lower,proto3" json:"Lower,omitempty"` Lower int64 `protobuf:"varint,2,opt,name=Lower,proto3" json:"Lower,omitempty"`
@ -284,7 +794,21 @@ type Range struct {
func (m *Range) Reset() { *m = Range{} } func (m *Range) Reset() { *m = Range{} }
func (m *Range) String() string { return proto.CompactTextString(m) } func (m *Range) String() string { return proto.CompactTextString(m) }
func (*Range) ProtoMessage() {} func (*Range) ProtoMessage() {}
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} } func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
func (m *Range) GetUpper() int64 {
if m != nil {
return m.Upper
}
return 0
}
func (m *Range) GetLower() int64 {
if m != nil {
return m.Lower
}
return 0
}
type AlertRule struct { type AlertRule 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"`
@ -296,7 +820,35 @@ type AlertRule struct {
func (m *AlertRule) Reset() { *m = AlertRule{} } func (m *AlertRule) Reset() { *m = AlertRule{} }
func (m *AlertRule) String() string { return proto.CompactTextString(m) } func (m *AlertRule) String() string { return proto.CompactTextString(m) }
func (*AlertRule) ProtoMessage() {} func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} } func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
func (m *AlertRule) GetID() string {
if m != nil {
return m.ID
}
return ""
}
func (m *AlertRule) GetJSON() string {
if m != nil {
return m.JSON
}
return ""
}
func (m *AlertRule) GetSrcID() int64 {
if m != nil {
return m.SrcID
}
return 0
}
func (m *AlertRule) GetKapaID() int64 {
if m != nil {
return m.KapaID
}
return 0
}
type User struct { type User struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
@ -306,7 +858,21 @@ type User struct {
func (m *User) Reset() { *m = User{} } func (m *User) Reset() { *m = User{} }
func (m *User) String() string { return proto.CompactTextString(m) } func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {} func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} } func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
func (m *User) GetID() uint64 {
if m != nil {
return m.ID
}
return 0
}
func (m *User) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func init() { func init() {
proto.RegisterType((*Source)(nil), "internal.Source") proto.RegisterType((*Source)(nil), "internal.Source")
@ -320,6 +886,7 @@ func init() {
proto.RegisterType((*Layout)(nil), "internal.Layout") proto.RegisterType((*Layout)(nil), "internal.Layout")
proto.RegisterType((*Cell)(nil), "internal.Cell") proto.RegisterType((*Cell)(nil), "internal.Cell")
proto.RegisterType((*Query)(nil), "internal.Query") proto.RegisterType((*Query)(nil), "internal.Query")
proto.RegisterType((*TimeShift)(nil), "internal.TimeShift")
proto.RegisterType((*Range)(nil), "internal.Range") proto.RegisterType((*Range)(nil), "internal.Range")
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule") proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
proto.RegisterType((*User)(nil), "internal.User") proto.RegisterType((*User)(nil), "internal.User")
@ -328,70 +895,73 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{ var fileDescriptorInternal = []byte{
// 1028 bytes of a gzipped FileDescriptorProto // 1082 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0x4f, 0x6f, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4,
0x14, 0xd7, 0xf8, 0x4f, 0x12, 0xbf, 0x74, 0x0b, 0x1a, 0xad, 0x58, 0xb3, 0x5c, 0x82, 0x05, 0x52, 0x13, 0x96, 0x63, 0x3b, 0x89, 0x2b, 0xb3, 0xf3, 0xfb, 0xa9, 0x59, 0xb1, 0x66, 0xb9, 0x04, 0x0b,
0x40, 0x6c, 0x41, 0xbb, 0x42, 0x42, 0xdc, 0xd2, 0x06, 0xad, 0x4a, 0xbb, 0x4b, 0x99, 0xb4, 0xe5, 0xa4, 0xf0, 0x67, 0x07, 0xb4, 0x2b, 0x24, 0xc4, 0x2d, 0x33, 0x41, 0xab, 0x61, 0x66, 0x97, 0x99,
0x84, 0x56, 0x13, 0xe7, 0xa5, 0xb5, 0xd6, 0x89, 0xcd, 0xd8, 0x6e, 0xe3, 0x6f, 0xc1, 0x27, 0x40, 0xce, 0xcc, 0x70, 0x42, 0xab, 0x4e, 0x52, 0x99, 0x58, 0xeb, 0xd8, 0xa6, 0xdd, 0x9e, 0x89, 0xdf,
0x42, 0xe2, 0xc4, 0x81, 0x03, 0x5f, 0x80, 0xfb, 0x7e, 0x2a, 0xf4, 0x66, 0xc6, 0x8e, 0xc3, 0x76, 0x82, 0x27, 0x40, 0x42, 0xe2, 0xc4, 0x81, 0x03, 0x2f, 0xc0, 0x9d, 0x17, 0xe2, 0x8a, 0xaa, 0xbb,
0xd1, 0x5e, 0xe0, 0x36, 0xbf, 0xf7, 0xc6, 0x6f, 0x66, 0xde, 0xef, 0xfd, 0x7e, 0x09, 0xec, 0x27, 0xed, 0x38, 0xec, 0x2c, 0xda, 0x0b, 0xdc, 0xfa, 0xab, 0xea, 0x54, 0x55, 0xd7, 0x57, 0xf5, 0xc5,
0xeb, 0x12, 0xd5, 0x5a, 0xa6, 0x07, 0xb9, 0xca, 0xca, 0x8c, 0x0f, 0x1a, 0x1c, 0xfd, 0xe1, 0x40, 0xb0, 0x1f, 0xa7, 0x0a, 0x65, 0x2a, 0x92, 0x83, 0x5c, 0x66, 0x2a, 0x63, 0xfd, 0x1a, 0x47, 0xbf,
0x6f, 0x96, 0x55, 0x2a, 0x46, 0xbe, 0x0f, 0xce, 0xf1, 0x34, 0x64, 0x23, 0x36, 0x76, 0x85, 0x73, 0x76, 0xa0, 0x3b, 0xcd, 0x4a, 0x39, 0x47, 0xb6, 0x0f, 0x9d, 0xe3, 0x49, 0xe8, 0x0c, 0x9d, 0x91,
0x3c, 0xe5, 0x1c, 0xbc, 0xe7, 0x72, 0x85, 0xa1, 0x33, 0x62, 0xe3, 0x40, 0xe8, 0x35, 0xc5, 0xce, 0xcb, 0x3b, 0xc7, 0x13, 0xc6, 0xc0, 0x7b, 0x2e, 0xd6, 0x18, 0x76, 0x86, 0xce, 0x28, 0xe0, 0xfa,
0xeb, 0x1c, 0x43, 0xd7, 0xc4, 0x68, 0xcd, 0x1f, 0xc2, 0xe0, 0xa2, 0xa0, 0x6a, 0x2b, 0x0c, 0x3d, 0x4c, 0xb6, 0x8b, 0x2a, 0xc7, 0xd0, 0x35, 0x36, 0x3a, 0xb3, 0x87, 0xd0, 0xbf, 0x2c, 0x28, 0xda,
0x1d, 0x6f, 0x31, 0xe5, 0xce, 0x64, 0x51, 0xdc, 0x66, 0x6a, 0x11, 0xfa, 0x26, 0xd7, 0x60, 0xfe, 0x1a, 0x43, 0x4f, 0xdb, 0x1b, 0x4c, 0xbe, 0x33, 0x51, 0x14, 0xb7, 0x99, 0x5c, 0x84, 0xbe, 0xf1,
0x2e, 0xb8, 0x17, 0xe2, 0x34, 0xec, 0xe9, 0x30, 0x2d, 0x79, 0x08, 0xfd, 0x29, 0x2e, 0x65, 0x95, 0xd5, 0x98, 0xfd, 0x1f, 0xdc, 0x4b, 0x7e, 0x1a, 0x76, 0xb5, 0x99, 0x8e, 0x2c, 0x84, 0xde, 0x04,
0x96, 0x61, 0x7f, 0xc4, 0xc6, 0x03, 0xd1, 0x40, 0xaa, 0x73, 0x8e, 0x29, 0x5e, 0x29, 0xb9, 0x0c, 0x97, 0xa2, 0x4c, 0x54, 0xd8, 0x1b, 0x3a, 0xa3, 0x3e, 0xaf, 0x21, 0xc5, 0xb9, 0xc0, 0x04, 0xaf,
0x07, 0xa6, 0x4e, 0x83, 0xf9, 0x01, 0xf0, 0xe3, 0x75, 0x81, 0x71, 0xa5, 0x70, 0xf6, 0x32, 0xc9, 0xa5, 0x58, 0x86, 0x7d, 0x13, 0xa7, 0xc6, 0xec, 0x00, 0xd8, 0x71, 0x5a, 0xe0, 0xbc, 0x94, 0x38,
0x2f, 0x51, 0x25, 0xcb, 0x3a, 0x0c, 0x74, 0x81, 0x3b, 0x32, 0x74, 0xca, 0x33, 0x2c, 0x25, 0x9d, 0x7d, 0x19, 0xe7, 0x57, 0x28, 0xe3, 0x65, 0x15, 0x06, 0x3a, 0xc0, 0x1d, 0x1e, 0xca, 0xf2, 0x0c,
0x0d, 0xba, 0x54, 0x03, 0x79, 0x04, 0x7b, 0xb3, 0x6b, 0xa9, 0x70, 0x31, 0xc3, 0x58, 0x61, 0x19, 0x95, 0xa0, 0xdc, 0xa0, 0x43, 0xd5, 0x90, 0x45, 0xb0, 0x37, 0x5d, 0x09, 0x89, 0x8b, 0x29, 0xce,
0x0e, 0x75, 0x7a, 0x27, 0x16, 0xfd, 0xcc, 0x20, 0x98, 0xca, 0xe2, 0x7a, 0x9e, 0x49, 0xb5, 0x78, 0x25, 0xaa, 0x70, 0xa0, 0xdd, 0x3b, 0xb6, 0xe8, 0x07, 0x07, 0x82, 0x89, 0x28, 0x56, 0xb3, 0x4c,
0xab, 0x9e, 0x3d, 0x02, 0x3f, 0xc6, 0x34, 0x2d, 0x42, 0x77, 0xe4, 0x8e, 0x87, 0x8f, 0x1f, 0x1c, 0xc8, 0xc5, 0x1b, 0xf5, 0xec, 0x11, 0xf8, 0x73, 0x4c, 0x92, 0x22, 0x74, 0x87, 0xee, 0x68, 0xf0,
0xb4, 0x64, 0xb4, 0x75, 0x8e, 0x30, 0x4d, 0x85, 0xd9, 0xc5, 0xbf, 0x80, 0xa0, 0xc4, 0x55, 0x9e, 0xf8, 0xc1, 0x41, 0x43, 0x46, 0x13, 0xe7, 0x08, 0x93, 0x84, 0x9b, 0x5b, 0xec, 0x33, 0x08, 0x14,
0xca, 0x12, 0x8b, 0xd0, 0xd3, 0x9f, 0xf0, 0xed, 0x27, 0xe7, 0x36, 0x25, 0xb6, 0x9b, 0xa2, 0xdf, 0xae, 0xf3, 0x44, 0x28, 0x2c, 0x42, 0x4f, 0xff, 0x84, 0x6d, 0x7f, 0x72, 0x61, 0x5d, 0x7c, 0x7b,
0x1d, 0xb8, 0xb7, 0x53, 0x8a, 0xef, 0x01, 0xdb, 0xe8, 0x5b, 0xf9, 0x82, 0x6d, 0x08, 0xd5, 0xfa, 0x29, 0xfa, 0xa5, 0x03, 0xf7, 0x76, 0x42, 0xb1, 0x3d, 0x70, 0x36, 0xba, 0x2a, 0x9f, 0x3b, 0x1b,
0x46, 0xbe, 0x60, 0x35, 0xa1, 0x5b, 0xcd, 0x9f, 0x2f, 0xd8, 0x2d, 0xa1, 0x6b, 0xcd, 0x9a, 0x2f, 0x42, 0x95, 0xae, 0xc8, 0xe7, 0x4e, 0x45, 0xe8, 0x56, 0xf3, 0xe7, 0x73, 0xe7, 0x96, 0xd0, 0x4a,
0xd8, 0x35, 0xff, 0x04, 0xfa, 0x3f, 0x55, 0xa8, 0x12, 0x2c, 0x42, 0x5f, 0x9f, 0xfc, 0xce, 0xf6, 0xb3, 0xe6, 0x73, 0x67, 0xc5, 0x3e, 0x84, 0xde, 0xf7, 0x25, 0xca, 0x18, 0x8b, 0xd0, 0xd7, 0x99,
0xe4, 0xef, 0x2b, 0x54, 0xb5, 0x68, 0xf2, 0xf4, 0x52, 0xcd, 0xb8, 0xa1, 0x4f, 0xaf, 0x29, 0x56, 0xff, 0xb7, 0xcd, 0x7c, 0x5e, 0xa2, 0xac, 0x78, 0xed, 0xa7, 0x97, 0x6a, 0xc6, 0x0d, 0x7d, 0xfa,
0xd2, 0x74, 0xf4, 0x4d, 0x8c, 0xd6, 0xb6, 0x43, 0x86, 0x33, 0xea, 0xd0, 0x97, 0xe0, 0xc9, 0x0d, 0x4c, 0x36, 0x45, 0xd3, 0xd1, 0x33, 0x36, 0x3a, 0xdb, 0x0e, 0x19, 0xce, 0xa8, 0x43, 0x9f, 0x83,
0x16, 0x61, 0xa0, 0xeb, 0x7f, 0xf8, 0x86, 0x66, 0x1c, 0x4c, 0x36, 0x58, 0x7c, 0xb3, 0x2e, 0x55, 0x27, 0x36, 0x58, 0x84, 0x81, 0x8e, 0xff, 0xde, 0x6b, 0x9a, 0x71, 0x30, 0xde, 0x60, 0xf1, 0x55,
0x2d, 0xf4, 0xf6, 0x87, 0x4f, 0x21, 0x68, 0x43, 0x34, 0x39, 0x2f, 0xb1, 0xd6, 0x0f, 0x0c, 0x04, 0xaa, 0x64, 0xc5, 0xf5, 0xf5, 0x87, 0x4f, 0x21, 0x68, 0x4c, 0x34, 0x39, 0x2f, 0xb1, 0xd2, 0x0f,
0x2d, 0xf9, 0x47, 0xe0, 0xdf, 0xc8, 0xb4, 0x32, 0x8d, 0x1f, 0x3e, 0xde, 0xdf, 0x96, 0x9d, 0x6c, 0x0c, 0x38, 0x1d, 0xd9, 0xfb, 0xe0, 0xdf, 0x88, 0xa4, 0x34, 0x8d, 0x1f, 0x3c, 0xde, 0xdf, 0x86,
0x92, 0x42, 0x98, 0xe4, 0xd7, 0xce, 0x57, 0x2c, 0xfa, 0x93, 0x81, 0x47, 0x31, 0x22, 0x3b, 0xc5, 0x1d, 0x6f, 0xe2, 0x82, 0x1b, 0xe7, 0x97, 0x9d, 0x2f, 0x9c, 0xe8, 0x37, 0x07, 0x3c, 0xb2, 0x11,
0x2b, 0x19, 0xd7, 0x87, 0x59, 0xb5, 0x5e, 0x14, 0x21, 0x1b, 0xb9, 0x63, 0x57, 0xec, 0xc4, 0xf8, 0xd9, 0x09, 0x5e, 0x8b, 0x79, 0x75, 0x98, 0x95, 0xe9, 0xa2, 0x08, 0x9d, 0xa1, 0x3b, 0x72, 0xf9,
0x7b, 0xd0, 0x9b, 0x9b, 0xac, 0x33, 0x72, 0xc7, 0x81, 0xb0, 0x88, 0xdf, 0x07, 0x3f, 0x95, 0x73, 0x8e, 0x8d, 0xbd, 0x0d, 0xdd, 0x99, 0xf1, 0x76, 0x86, 0xee, 0x28, 0xe0, 0x16, 0xb1, 0xfb, 0xe0,
0x4c, 0xad, 0x0e, 0x0c, 0xa0, 0xdd, 0xb9, 0xc2, 0x65, 0xb2, 0xb1, 0x32, 0xb0, 0x88, 0xe2, 0x45, 0x27, 0x62, 0x86, 0x89, 0xdd, 0x03, 0x03, 0xe8, 0x76, 0x2e, 0x71, 0x19, 0x6f, 0xec, 0x1a, 0x58,
0xb5, 0xa4, 0xb8, 0x91, 0x80, 0x45, 0xd4, 0xae, 0xb9, 0x2c, 0xda, 0x16, 0xd2, 0x9a, 0x2a, 0x17, 0x44, 0xf6, 0xa2, 0x5c, 0x92, 0xdd, 0xac, 0x80, 0x45, 0xd4, 0xae, 0x99, 0x28, 0x9a, 0x16, 0xd2,
0xb1, 0x4c, 0x9b, 0x1e, 0x1a, 0x10, 0xfd, 0xc5, 0x68, 0xfe, 0x0d, 0xdf, 0x9d, 0x99, 0x33, 0x1d, 0x99, 0x22, 0x17, 0x73, 0x91, 0xd4, 0x3d, 0x34, 0x20, 0xfa, 0xdd, 0xa1, 0xf9, 0x37, 0x7c, 0xb7,
0x7d, 0x1f, 0x06, 0x34, 0x0b, 0x2f, 0x6e, 0xa4, 0xb2, 0x73, 0xd7, 0x27, 0x7c, 0x29, 0x15, 0xff, 0x66, 0xce, 0x74, 0xf4, 0x1d, 0xe8, 0xd3, 0x2c, 0xbc, 0xb8, 0x11, 0xd2, 0xce, 0x5d, 0x8f, 0xf0,
0x1c, 0x7a, 0xfa, 0xe5, 0x77, 0xcc, 0x5e, 0x53, 0xee, 0x92, 0xf2, 0xc2, 0x6e, 0x6b, 0x19, 0xf4, 0x95, 0x90, 0xec, 0x53, 0xe8, 0xea, 0x97, 0xdf, 0x31, 0x7b, 0x75, 0xb8, 0x2b, 0xf2, 0x73, 0x7b,
0x3a, 0x0c, 0xb6, 0x8f, 0xf5, 0xbb, 0x8f, 0x7d, 0x04, 0x3e, 0x8d, 0x42, 0xad, 0x6f, 0x7f, 0x67, 0xad, 0x61, 0xd0, 0x6b, 0x31, 0xd8, 0x3c, 0xd6, 0x6f, 0x3f, 0xf6, 0x11, 0xf8, 0x34, 0x0a, 0x95,
0x65, 0x33, 0x30, 0x66, 0x57, 0x74, 0x01, 0xf7, 0x76, 0x4e, 0x6c, 0x4f, 0x62, 0xbb, 0x27, 0x6d, 0xae, 0xfe, 0xce, 0xc8, 0x66, 0x60, 0xcc, 0xad, 0xe8, 0x12, 0xee, 0xed, 0x64, 0x6c, 0x32, 0x39,
0x59, 0x0c, 0x2c, 0x6b, 0xa4, 0xfd, 0x02, 0x53, 0x8c, 0x4b, 0x5c, 0xe8, 0x7e, 0x0f, 0x44, 0x8b, 0xbb, 0x99, 0xb6, 0x2c, 0x06, 0x96, 0x35, 0xda, 0xfd, 0x02, 0x13, 0x9c, 0x2b, 0x5c, 0xe8, 0x7e,
0xa3, 0x5f, 0xd9, 0xb6, 0xae, 0x3e, 0x8f, 0xd4, 0x1d, 0x67, 0xab, 0x95, 0x5c, 0x2f, 0x6c, 0xe9, 0xf7, 0x79, 0x83, 0xa3, 0x9f, 0x9c, 0x6d, 0x5c, 0x9d, 0x8f, 0xb6, 0x7b, 0x9e, 0xad, 0xd7, 0x22,
0x06, 0x52, 0xdf, 0x16, 0x73, 0x5b, 0xda, 0x59, 0xcc, 0x09, 0xab, 0xdc, 0x32, 0xe8, 0xa8, 0x9c, 0x5d, 0xd8, 0xd0, 0x35, 0xa4, 0xbe, 0x2d, 0x66, 0x36, 0x74, 0x67, 0x31, 0x23, 0x2c, 0x73, 0xcb,
0x8f, 0x60, 0xb8, 0x42, 0x59, 0x54, 0x0a, 0x57, 0xb8, 0x2e, 0x6d, 0x0b, 0xba, 0x21, 0xfe, 0x00, 0x60, 0x47, 0xe6, 0x6c, 0x08, 0x83, 0x35, 0x8a, 0xa2, 0x94, 0xb8, 0xc6, 0x54, 0xd9, 0x16, 0xb4,
0xfa, 0xa5, 0xbc, 0x7a, 0x41, 0xb3, 0x67, 0x99, 0x2c, 0xe5, 0xd5, 0x09, 0xd6, 0xfc, 0x03, 0x08, 0x4d, 0xec, 0x01, 0xf4, 0x94, 0xb8, 0x7e, 0x41, 0xb3, 0x67, 0x99, 0x54, 0xe2, 0xfa, 0x04, 0x2b,
0x96, 0x09, 0xa6, 0x0b, 0x9d, 0x32, 0x74, 0x0e, 0x74, 0xe0, 0x04, 0xeb, 0xe8, 0x37, 0x06, 0xbd, 0xf6, 0x2e, 0x04, 0xcb, 0x18, 0x93, 0x85, 0x76, 0x19, 0x3a, 0xfb, 0xda, 0x70, 0x82, 0x55, 0xf4,
0x19, 0xaa, 0x1b, 0x54, 0x6f, 0x65, 0x17, 0x5d, 0x3b, 0x75, 0xff, 0xc5, 0x4e, 0xbd, 0xbb, 0xed, 0xb3, 0x03, 0xdd, 0x29, 0xca, 0x1b, 0x94, 0x6f, 0x24, 0x17, 0x6d, 0x39, 0x75, 0xff, 0x41, 0x4e,
0xd4, 0xdf, 0xda, 0xe9, 0x7d, 0xf0, 0x67, 0x2a, 0x3e, 0x9e, 0xea, 0x1b, 0xb9, 0xc2, 0x00, 0x9a, 0xbd, 0xbb, 0xe5, 0xd4, 0xdf, 0xca, 0xe9, 0x7d, 0xf0, 0xa7, 0x72, 0x7e, 0x3c, 0xd1, 0x15, 0xb9,
0xc6, 0x49, 0x5c, 0x26, 0x37, 0x68, 0x3d, 0xd6, 0xa2, 0xe8, 0x17, 0x06, 0xbd, 0x53, 0x59, 0x67, 0xdc, 0x00, 0x9a, 0xc6, 0xf1, 0x5c, 0xc5, 0x37, 0x68, 0x35, 0xd6, 0xa2, 0xe8, 0x47, 0x07, 0xba,
0x55, 0xf9, 0xda, 0x84, 0x8d, 0x60, 0x38, 0xc9, 0xf3, 0x34, 0x89, 0x65, 0x99, 0x64, 0x6b, 0x7b, 0xa7, 0xa2, 0xca, 0x4a, 0xf5, 0xca, 0x84, 0x0d, 0x61, 0x30, 0xce, 0xf3, 0x24, 0x9e, 0x0b, 0x15,
0xdb, 0x6e, 0x88, 0x76, 0x3c, 0xeb, 0xf4, 0xce, 0xdc, 0xbb, 0x1b, 0x22, 0x85, 0x1e, 0x69, 0x17, 0x67, 0xa9, 0xad, 0xb6, 0x6d, 0xa2, 0x1b, 0xcf, 0x5a, 0xbd, 0x33, 0x75, 0xb7, 0x4d, 0xb4, 0xa1,
0x34, 0x96, 0xd6, 0x51, 0xa8, 0x31, 0x3f, 0x9d, 0xa4, 0x07, 0x4e, 0xaa, 0x32, 0x5b, 0xa6, 0xd9, 0x47, 0x5a, 0x05, 0x8d, 0xa4, 0xb5, 0x36, 0xd4, 0x88, 0x9f, 0x76, 0xd2, 0x03, 0xc7, 0xa5, 0xca,
0xad, 0x7e, 0xc9, 0x40, 0xb4, 0x38, 0x7a, 0xe5, 0x80, 0xf7, 0x7f, 0xb9, 0xdb, 0x1e, 0xb0, 0xc4, 0x96, 0x49, 0x76, 0xab, 0x5f, 0xd2, 0xe7, 0x0d, 0x8e, 0xfe, 0xe8, 0x80, 0xf7, 0x5f, 0xa9, 0xdb,
0x12, 0xc9, 0x92, 0xd6, 0xeb, 0xfa, 0x1d, 0xaf, 0x0b, 0xa1, 0x5f, 0x2b, 0xb9, 0xbe, 0xc2, 0x22, 0x1e, 0x38, 0xb1, 0x25, 0xd2, 0x89, 0x1b, 0xad, 0xeb, 0xb5, 0xb4, 0x2e, 0x84, 0x5e, 0x25, 0x45,
0x1c, 0x68, 0xe7, 0x68, 0xa0, 0xce, 0x68, 0x8d, 0x18, 0x93, 0x0b, 0x44, 0x03, 0xdb, 0x99, 0x87, 0x7a, 0x8d, 0x45, 0xd8, 0xd7, 0xca, 0x51, 0x43, 0xed, 0xd1, 0x3b, 0x62, 0x44, 0x2e, 0xe0, 0x35,
0xce, 0xcc, 0x7f, 0x66, 0xfd, 0x70, 0xa8, 0x6f, 0x14, 0xee, 0xb6, 0xe5, 0xbf, 0xb3, 0xc1, 0x57, 0x6c, 0x66, 0x1e, 0x5a, 0x33, 0xff, 0x89, 0xd5, 0xc3, 0x81, 0xae, 0x28, 0xdc, 0x6d, 0xcb, 0xbf,
0x0c, 0xfc, 0x56, 0x30, 0x47, 0xbb, 0x82, 0x39, 0xda, 0x0a, 0x66, 0x7a, 0xd8, 0x08, 0x66, 0x7a, 0x27, 0x83, 0x7f, 0x3a, 0xe0, 0x37, 0x0b, 0x73, 0xb4, 0xbb, 0x30, 0x47, 0xdb, 0x85, 0x99, 0x1c,
0x48, 0x58, 0x9c, 0x35, 0x82, 0x11, 0x67, 0x44, 0xd6, 0x53, 0x95, 0x55, 0xf9, 0x61, 0x6d, 0x58, 0xd6, 0x0b, 0x33, 0x39, 0x24, 0xcc, 0xcf, 0xea, 0x85, 0xe1, 0x67, 0x44, 0xd6, 0x53, 0x99, 0x95,
0x0d, 0x44, 0x8b, 0x69, 0xca, 0x7e, 0xb8, 0x46, 0x65, 0x5b, 0x1d, 0x08, 0x8b, 0x68, 0x26, 0x4f, 0xf9, 0x61, 0x65, 0x58, 0x0d, 0x78, 0x83, 0x69, 0xca, 0xbe, 0x5d, 0xa1, 0xb4, 0xad, 0x0e, 0xb8,
0xb5, 0x99, 0x98, 0xe6, 0x1a, 0xc0, 0x3f, 0x06, 0x5f, 0x50, 0xf3, 0x74, 0x87, 0x77, 0x78, 0xd1, 0x45, 0x34, 0x93, 0xa7, 0x5a, 0x4c, 0x4c, 0x73, 0x0d, 0x60, 0x1f, 0x80, 0xcf, 0xa9, 0x79, 0xba,
0x61, 0x61, 0xb2, 0x54, 0xd4, 0xfc, 0x57, 0xb1, 0xbf, 0x27, 0x16, 0x45, 0x4f, 0xec, 0xe7, 0x54, 0xc3, 0x3b, 0xbc, 0x68, 0x33, 0x37, 0x5e, 0x0a, 0x6a, 0xbe, 0x55, 0xec, 0xff, 0x49, 0xfd, 0xe5,
0xfd, 0x22, 0xcf, 0x51, 0x59, 0x89, 0x19, 0xa0, 0xcf, 0xcc, 0x6e, 0xd1, 0xb8, 0xa3, 0x2b, 0x0c, 0xf2, 0x31, 0x74, 0xa7, 0xab, 0x78, 0xa9, 0xea, 0x7f, 0x95, 0xb7, 0x5a, 0x62, 0x14, 0xaf, 0x51,
0x88, 0x7e, 0x84, 0x60, 0x92, 0xa2, 0x2a, 0x45, 0x95, 0xbe, 0xee, 0xa9, 0x1c, 0xbc, 0x6f, 0x67, 0xfb, 0xb8, 0xbd, 0x12, 0x9d, 0x43, 0xd0, 0x18, 0xb7, 0xe5, 0x38, 0xed, 0x72, 0x18, 0x78, 0x97,
0xdf, 0x3d, 0x6f, 0x84, 0x49, 0xeb, 0xad, 0x9c, 0xdc, 0x7f, 0xc8, 0xe9, 0x44, 0xe6, 0xf2, 0x78, 0x69, 0xac, 0xea, 0xb5, 0xa4, 0x33, 0x3d, 0xf6, 0xbc, 0x14, 0xa9, 0x8a, 0x55, 0x55, 0xaf, 0x65,
0xaa, 0xe7, 0xcc, 0x15, 0x16, 0x45, 0x9f, 0x82, 0x47, 0xb2, 0xed, 0x54, 0xf6, 0xde, 0x24, 0xf9, 0x8d, 0xa3, 0x27, 0xb6, 0x7c, 0x0a, 0x77, 0x99, 0xe7, 0x28, 0xed, 0x8a, 0x1b, 0xa0, 0x93, 0x64,
0x79, 0x4f, 0xff, 0x2b, 0x7b, 0xf2, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb7, 0x59, 0x2e, 0xc0, 0xb7, 0x68, 0xd4, 0xd9, 0xe5, 0x06, 0x44, 0xdf, 0x41, 0x30, 0x4e, 0x50, 0x2a, 0x5e, 0x26, 0xaf,
0xa7, 0x09, 0x00, 0x00, 0x6a, 0x3a, 0x03, 0xef, 0xeb, 0xe9, 0x37, 0xcf, 0xeb, 0x0a, 0xe8, 0xbc, 0x5d, 0x67, 0xf7, 0x6f,
0xeb, 0x7c, 0x22, 0x72, 0x71, 0x3c, 0xd1, 0x73, 0xee, 0x72, 0x8b, 0xa2, 0x8f, 0xc0, 0x23, 0xd9,
0x68, 0x45, 0xf6, 0x5e, 0x27, 0x39, 0xb3, 0xae, 0xfe, 0x2a, 0x7c, 0xf2, 0x57, 0x00, 0x00, 0x00,
0xff, 0xff, 0xda, 0x20, 0xfc, 0x99, 0x27, 0x0a, 0x00, 0x00,
} }

View File

@ -23,15 +23,15 @@ message Dashboard {
} }
message DashboardCell { message DashboardCell {
int32 x = 1; // X-coordinate of Cell in the Dashboard int32 x = 1; // X-coordinate of Cell in the Dashboard
int32 y = 2; // Y-coordinate of Cell in the Dashboard int32 y = 2; // Y-coordinate of Cell in the Dashboard
int32 w = 3; // Width of Cell in the Dashboard int32 w = 3; // Width of Cell in the Dashboard
int32 h = 4; // Height of Cell in the Dashboard int32 h = 4; // Height of Cell in the Dashboard
repeated Query queries = 5; // Time-series data queries for Dashboard repeated Query queries = 5; // Time-series data queries for Dashboard
string name = 6; // User-facing name for this Dashboard string name = 6; // User-facing name for this Dashboard
string type = 7; // Dashboard visualization type string type = 7; // Dashboard visualization type
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6 string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
} }
message Axis { message Axis {
@ -54,18 +54,18 @@ message Template {
} }
message TemplateValue { message TemplateValue {
string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
string value = 2; // Value is the specific value used to replace a template in an InfluxQL query string value = 2; // Value is the specific value used to replace a template in an InfluxQL query
bool selected = 3; // Selected states that this variable has been picked to use for replacement bool selected = 3; // Selected states that this variable has been picked to use for replacement
} }
message TemplateQuery { message TemplateQuery {
string command = 1; // Command is the query itself string command = 1; // Command is the query itself
string db = 2; // DB the database for the query (optional) string db = 2; // DB the database for the query (optional)
string rp = 3; // RP is a retention policy and optional; string rp = 3; // RP is a retention policy and optional;
string measurement = 4; // Measurement is the optinally selected measurement for the query string measurement = 4; // Measurement is the optinally selected measurement for the query
string tag_key = 5; // TagKey is the optionally selected tag key for the query string tag_key = 5; // TagKey is the optionally selected tag key for the query
string field_key = 6; // FieldKey is the optionally selected field key for the query string field_key = 6; // FieldKey is the optionally selected field key for the query
} }
message Server { message Server {
@ -101,31 +101,38 @@ message Cell {
} }
message Query { message Query {
string Command = 1; // Command is the query itself string Command = 1; // Command is the query itself
string DB = 2; // DB the database for the query (optional) string DB = 2; // DB the database for the query (optional)
string RP = 3; // RP is a retention policy and optional; string RP = 3; // RP is a retention policy and optional;
repeated string GroupBys= 4; // GroupBys define the groups to combine in the query repeated string GroupBys = 4; // GroupBys define the groups to combine in the query
repeated string Wheres = 5; // Wheres define the restrictions on the query repeated string Wheres = 5; // Wheres define the restrictions on the query
string Label = 6; // Label is the name of the Y-Axis string Label = 6; // Label is the name of the Y-Axis
Range Range = 7; // Range is the upper and lower bound of the Y-Axis Range Range = 7; // Range is the upper and lower bound of the Y-Axis
string Source = 8; // Source is the optional URI to the data source string Source = 8; // Source is the optional URI to the data source
repeated TimeShift Shifts = 9; // TimeShift represents a shift to apply to an influxql query's time range
}
message TimeShift {
string Label = 1; // Label user facing description
string Unit = 2; // Unit influxql time unit representation i.e. ms, s, m, h, d
string Quantity = 3; // Quantity number of units
} }
message Range { message Range {
int64 Upper = 1; // Upper is the upper-bound of the range int64 Upper = 1; // Upper is the upper-bound of the range
int64 Lower = 2; // Lower is the lower-bound of the range int64 Lower = 2; // Lower is the lower-bound of the range
} }
message AlertRule { message AlertRule {
string ID = 1; // ID is the unique ID of this alert rule string ID = 1; // ID is the unique ID of this alert rule
string JSON = 2; // JSON byte representation of the alert string JSON = 2; // JSON byte representation of the alert
int64 SrcID = 3; // SrcID is the id of the source this alert is associated with int64 SrcID = 3; // SrcID is the id of the source this alert is associated with
int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with
} }
message User { message User {
uint64 ID = 1; // ID is the unique ID of this user uint64 ID = 1; // ID is the unique ID of this user
string Name = 2; // Name is the user's login name string Name = 2; // Name is the user's login name
} }
// The following is a vim modeline, it autoconfigures vim to have the // The following is a vim modeline, it autoconfigures vim to have the

View File

@ -163,6 +163,7 @@ func Test_MarshalDashboard(t *testing.T) {
Upper: int64(100), Upper: int64(100),
}, },
Source: "/chronograf/v1/sources/1", Source: "/chronograf/v1/sources/1",
Shifts: []chronograf.TimeShift{},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
@ -210,6 +211,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Range: &chronograf.Range{ Range: &chronograf.Range{
Upper: int64(100), Upper: int64(100),
}, },
Shifts: []chronograf.TimeShift{},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
@ -241,6 +243,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Range: &chronograf.Range{ Range: &chronograf.Range{
Upper: int64(100), Upper: int64(100),
}, },
Shifts: []chronograf.TimeShift{},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
@ -285,6 +288,7 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
Range: &chronograf.Range{ Range: &chronograf.Range{
Upper: int64(100), Upper: int64(100),
}, },
Shifts: []chronograf.TimeShift{},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
@ -316,6 +320,7 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
Range: &chronograf.Range{ Range: &chronograf.Range{
Upper: int64(100), Upper: int64(100),
}, },
Shifts: []chronograf.TimeShift{},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{

View File

@ -1,21 +1,10 @@
package chronograf package chronograf
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors"
"fmt"
"io" "io"
"net/http" "net/http"
"regexp"
"strconv"
"strings"
"time" "time"
"unicode"
"unicode/utf8"
"github.com/influxdata/influxdb/influxql"
) )
// General errors. // General errors.
@ -136,196 +125,17 @@ type Range struct {
Lower int64 `json:"lower"` // Lower is the lower bound Lower int64 `json:"lower"` // Lower is the lower bound
} }
type TemplateVariable interface {
fmt.Stringer
Name() string // returns the variable name
Precedence() uint // ordinal indicating precedence level for replacement
}
type ExecutableVar interface {
Exec(string)
}
// TemplateValue is a value use to replace a template in an InfluxQL query // TemplateValue is a value use to replace a template in an InfluxQL query
type BasicTemplateValue struct { type TemplateValue struct {
Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query
Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement
} }
// TemplateVar is a named variable within an InfluxQL query to be replaced with Values // TemplateVar is a named variable within an InfluxQL query to be replaced with Values
type BasicTemplateVar struct { type TemplateVar struct {
Var string `json:"tempVar"` // Var is the string to replace within InfluxQL Var string `json:"tempVar"` // Var is the string to replace within InfluxQL
Values []BasicTemplateValue `json:"values"` // Values are the replacement values within InfluxQL Values []TemplateValue `json:"values"` // Values are the replacement values within InfluxQL
}
func (t BasicTemplateVar) Name() string {
return t.Var
}
// String converts the template variable into a correct InfluxQL string based
// on its type
func (t BasicTemplateVar) String() string {
if len(t.Values) == 0 {
return ""
}
switch t.Values[0].Type {
case "tagKey", "fieldKey", "measurement", "database":
return `"` + t.Values[0].Value + `"`
case "tagValue", "timeStamp":
return `'` + t.Values[0].Value + `'`
case "csv", "constant":
return t.Values[0].Value
default:
return ""
}
}
func (t BasicTemplateVar) Precedence() uint {
return 0
}
type GroupByVar struct {
Var string `json:"tempVar"` // the name of the variable as present in the query
Duration time.Duration `json:"duration,omitempty"` // the Duration supplied by the query
Resolution uint `json:"resolution"` // the available screen resolution to render the results of this query
ReportingInterval time.Duration `json:"reportingInterval,omitempty"` // the interval at which data is reported to this series
}
// Exec is responsible for extracting the Duration from the query
func (g *GroupByVar) Exec(query string) {
whereClause := "WHERE"
start := strings.Index(query, whereClause)
if start == -1 {
// no where clause
return
}
// reposition start to after the 'where' keyword
durStr := query[start+len(whereClause):]
// attempt to parse out a relative time range
// locate duration literal start
prefix := "time > now() - "
lowerDuration, err := g.parseRelative(durStr, prefix)
if err == nil {
prefix := "time < now() - "
upperDuration, err := g.parseRelative(durStr, prefix)
if err != nil {
g.Duration = lowerDuration
return
}
g.Duration = lowerDuration - upperDuration
if g.Duration < 0 {
g.Duration = -g.Duration
}
}
dur, err := g.parseAbsolute(durStr)
if err == nil {
// we found an absolute time range
g.Duration = dur
}
}
// parseRelative locates and extracts a duration value from a fragment of an
// InfluxQL query following the "where" keyword. For example, in the fragment
// "time > now() - 180d GROUP BY :interval:", parseRelative would return a
// duration equal to 180d
func (g *GroupByVar) parseRelative(fragment string, prefix string) (time.Duration, error) {
start := strings.Index(fragment, prefix)
if start == -1 {
return time.Duration(0), errors.New("not a relative duration")
}
// reposition to duration literal
durFragment := fragment[start+len(prefix):]
// init counters
pos := 0
// locate end of duration literal
for pos < len(durFragment) {
rn, _ := utf8.DecodeRuneInString(durFragment[pos:])
if unicode.IsSpace(rn) {
break
}
pos++
}
// attempt to parse what we suspect is a duration literal
dur, err := influxql.ParseDuration(durFragment[:pos])
if err != nil {
return dur, err
}
return dur, nil
}
// parseAbsolute will determine the duration between two absolute timestamps
// found within an InfluxQL fragment following the "where" keyword. For
// example, the fragement "time > '1985-10-25T00:01:21-0800 and time <
// '1985-10-25T00:01:22-0800'" would yield a duration of 1m'
func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) {
timePtn := `time\s[>|<]\s'([0-9\-T\:\.Z]+)'` // Playground: http://gobular.com/x/208f66bd-1889-4269-ab47-1efdfeeb63f0
re, err := regexp.Compile(timePtn)
if err != nil {
// this is a developer error and should complain loudly
panic("Bad Regex: err:" + err.Error())
}
if !re.Match([]byte(fragment)) {
return time.Duration(0), errors.New("absolute duration not found")
}
// extract at most two times
matches := re.FindAll([]byte(fragment), 2)
// parse out absolute times
durs := make([]time.Time, 0, 2)
for _, match := range matches {
durStr := re.FindSubmatch(match)
if tm, err := time.Parse(time.RFC3339Nano, string(durStr[1])); err == nil {
durs = append(durs, tm)
}
}
if len(durs) == 1 {
durs = append(durs, time.Now())
}
// reject more than 2 times found
if len(durs) != 2 {
return time.Duration(0), errors.New("must provide exactly two absolute times")
}
dur := durs[1].Sub(durs[0])
return dur, nil
}
func (g *GroupByVar) String() string {
// The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3
// Number of points given the pixels
pixels := float64(g.Resolution) / 3.0
msPerPixel := float64(g.Duration/time.Millisecond) / pixels
secPerPixel := float64(g.Duration/time.Second) / pixels
if secPerPixel < 1.0 {
if msPerPixel < 1.0 {
msPerPixel = 1.0
}
return "time(" + strconv.FormatInt(int64(msPerPixel), 10) + "ms)"
}
// If groupby is more than 1 second round to the second
return "time(" + strconv.FormatInt(int64(secPerPixel), 10) + "s)"
}
func (g *GroupByVar) Name() string {
return g.Var
}
func (g *GroupByVar) Precedence() uint {
return 1
} }
// TemplateID is the unique ID used to identify a template // TemplateID is the unique ID used to identify a template
@ -333,7 +143,7 @@ type TemplateID string
// Template represents a series of choices to replace TemplateVars within InfluxQL // Template represents a series of choices to replace TemplateVars within InfluxQL
type Template struct { type Template struct {
BasicTemplateVar TemplateVar
ID TemplateID `json:"id"` // ID is the unique ID associated with this template ID TemplateID `json:"id"` // ID is the unique ID associated with this template
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases
Label string `json:"label"` // Label is a user-facing description of the Template Label string `json:"label"` // Label is a user-facing description of the Template
@ -342,69 +152,15 @@ type Template struct {
// Query retrieves a Response from a TimeSeries. // Query retrieves a Response from a TimeSeries.
type Query struct { type Query struct {
Command string `json:"query"` // Command is the query itself Command string `json:"query"` // Command is the query itself
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used. DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used. RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
TemplateVars TemplateVars `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
Resolution uint `json:"resolution,omitempty"` // Resolution is the available screen resolution to render query results Resolution uint `json:"resolution,omitempty"` // Resolution is the available screen resolution to render query results
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
}
// TemplateVars are a heterogeneous collection of different TemplateVariables
// with the capability to decode arbitrary JSON into the appropriate template
// variable type
type TemplateVars []TemplateVariable
func (t *TemplateVars) UnmarshalJSON(text []byte) error {
// TODO: Need to test that server throws an error when :interval:'s Resolution or ReportingInterval or zero-value
rawVars := bytes.NewReader(text)
dec := json.NewDecoder(rawVars)
// read open bracket
rawTok, err := dec.Token()
if err != nil {
return err
}
tok, isDelim := rawTok.(json.Delim)
if !isDelim || tok != '[' {
return errors.New("Expected JSON array, but found " + tok.String())
}
for dec.More() {
var halfBakedVar json.RawMessage
err := dec.Decode(&halfBakedVar)
if err != nil {
return err
}
var agb GroupByVar
err = json.Unmarshal(halfBakedVar, &agb)
if err != nil {
return err
}
// ensure that we really have a GroupByVar
if agb.Resolution != 0 {
(*t) = append(*t, &agb)
continue
}
var tvar BasicTemplateVar
err = json.Unmarshal(halfBakedVar, &tvar)
if err != nil {
return err
}
// ensure that we really have a BasicTemplateVar
if len(tvar.Values) != 0 {
(*t) = append(*t, tvar)
}
}
return nil
} }
// DashboardQuery includes state for the query builder. This is a transition // DashboardQuery includes state for the query builder. This is a transition
@ -415,6 +171,7 @@ type DashboardQuery struct {
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
Source string `json:"source"` // Source is the optional URI to the data source for this queryConfig Source string `json:"source"` // Source is the optional URI to the data source for this queryConfig
Shifts []TimeShift `json:"-"` // Shifts represents shifts to apply to an influxql query's time range. Clients expect the shift to be in the generated QueryConfig
} }
// TemplateQuery is used to retrieve choices for template replacement // TemplateQuery is used to retrieve choices for template replacement
@ -528,6 +285,13 @@ type DurationRange struct {
Lower string `json:"lower"` Lower string `json:"lower"`
} }
// TimeShift represents a shift to apply to an influxql query's time range
type TimeShift struct {
Label string `json:"label"` // Label user facing description
Unit string `json:"unit"` // Unit influxql time unit representation i.e. ms, s, m, h, d
Quantity string `json:"quantity"` // Quantity number of units
}
// QueryConfig represents UI query from the data explorer // QueryConfig represents UI query from the data explorer
type QueryConfig struct { type QueryConfig struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
@ -541,6 +305,7 @@ type QueryConfig struct {
Fill string `json:"fill,omitempty"` Fill string `json:"fill,omitempty"`
RawText *string `json:"rawText"` RawText *string `json:"rawText"`
Range *DurationRange `json:"range"` Range *DurationRange `json:"range"`
Shifts []TimeShift `json:"shifts"`
} }
// KapacitorNode adds arguments and properties to an alert // KapacitorNode adds arguments and properties to an alert

View File

@ -1,63 +0,0 @@
package chronograf_test
import (
"testing"
"github.com/influxdata/chronograf"
)
func Test_GroupByVar(t *testing.T) {
gbvTests := []struct {
name string
query string
want string
resolution uint // the screen resolution to render queries into
}{
{
name: "relative time only lower bound with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY :interval:",
resolution: 1000,
want: "time(259s)",
},
{
name: "relative time with relative upper bound with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY :interval:",
resolution: 1000,
want: "time(180ms)",
},
{
name: "relative time with relative lower bound and now upper with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY :interval:",
resolution: 1000,
want: "time(259s)",
},
{
name: "absolute time with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
resolution: 1000,
want: "time(180ms)",
},
{
name: "absolute time with nano seconds and zero duraiton",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY :interval:",
resolution: 1000,
want: "time(1ms)",
},
}
for _, test := range gbvTests {
t.Run(test.name, func(t *testing.T) {
gbv := chronograf.GroupByVar{
Var: ":interval:",
Resolution: test.resolution,
}
gbv.Exec(test.query)
got := gbv.String()
if got != test.want {
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", test.name, test.want, got)
}
})
}
}

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
) )
@ -55,7 +56,10 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
command := q.Command command := q.Command
// TODO(timraymond): move this upper Query() function // TODO(timraymond): move this upper Query() function
if len(q.TemplateVars) > 0 { if len(q.TemplateVars) > 0 {
command = TemplateReplace(q.Command, q.TemplateVars) command, err = TemplateReplace(q.Command, q.TemplateVars, time.Now())
if err != nil {
return nil, err
}
} }
logs := c.Logger. logs := c.Logger.
WithField("component", "proxy"). WithField("component", "proxy").

View File

@ -276,11 +276,11 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
called = false called = false
q = "" q = ""
query = chronograf.Query{ query = chronograf.Query{
Command: "select $field from cpu", Command: "select :field: from cpu",
TemplateVars: chronograf.TemplateVars{ TemplateVars: []chronograf.TemplateVar{
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$field", Var: ":field:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Value: "usage_user", Value: "usage_user",
Type: "fieldKey", Type: "fieldKey",

View File

@ -10,6 +10,52 @@ import (
"github.com/influxdata/influxdb/influxql" "github.com/influxdata/influxdb/influxql"
) )
func TimeRangeAsEpochNano(expr influxql.Expr, now time.Time) (min, max int64, err error) {
tmin, tmax, err := influxql.TimeRange(expr)
if err != nil {
return 0, 0, err
}
if tmin.IsZero() {
min = time.Unix(0, influxql.MinTime).UnixNano()
} else {
min = tmin.UnixNano()
}
if tmax.IsZero() {
max = now.UnixNano()
} else {
max = tmax.UnixNano()
}
return
}
const WhereToken = "WHERE"
func ParseTime(influxQL string, now time.Time) (time.Duration, error) {
start := strings.Index(strings.ToUpper(influxQL), WhereToken)
if start == -1 {
return 0, fmt.Errorf("not a relative duration")
}
start += len(WhereToken)
where := influxQL[start:]
cond, err := influxql.ParseExpr(where)
if err != nil {
return 0, err
}
nowVal := &influxql.NowValuer{
Now: now,
}
cond = influxql.Reduce(cond, nowVal)
min, max, err := TimeRangeAsEpochNano(cond, now)
if err != nil {
return 0, err
}
dur := time.Duration(max - min)
if dur < 0 {
dur = 0
}
return dur, nil
}
// Convert changes an InfluxQL query to a QueryConfig // Convert changes an InfluxQL query to a QueryConfig
func Convert(influxQL string) (chronograf.QueryConfig, error) { func Convert(influxQL string) (chronograf.QueryConfig, error) {
itsDashboardTime := false itsDashboardTime := false

View File

@ -2,6 +2,7 @@ package influx
import ( import (
"testing" "testing"
"time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
@ -767,3 +768,43 @@ func TestConvert(t *testing.T) {
}) })
} }
} }
func TestParseTime(t *testing.T) {
tests := []struct {
name string
influxQL string
now string
want time.Duration
wantErr bool
}{
{
name: "time equal",
now: "2000-01-01T00:00:00Z",
influxQL: `SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h and time < now() - 1h GROUP BY :interval: FILL(null);`,
want: 0,
},
{
name: "time shifted by one hour",
now: "2000-01-01T00:00:00Z",
influxQL: `SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h - 1h and time < now() - 1h GROUP BY :interval: FILL(null);`,
want: 3599999999998,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now, err := time.Parse(time.RFC3339, tt.now)
if err != nil {
t.Fatalf("%v", err)
}
got, err := ParseTime(tt.influxQL, now)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Logf("%d", got)
t.Errorf("ParseTime() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,40 +1,106 @@
package influx package influx
import ( import (
"sort"
"strconv"
"strings" "strings"
"time"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
) )
// TemplateReplace replaces templates with values within the query string func SortTemplates(ts []chronograf.TemplateVar) []chronograf.TemplateVar {
func TemplateReplace(query string, templates chronograf.TemplateVars) string { sort.Slice(ts, func(i, j int) bool {
tvarsByPrecedence := make(map[uint]chronograf.TemplateVars, len(templates)) if len(ts[i].Values) != len(ts[j].Values) {
maxPrecedence := uint(0) return len(ts[i].Values) < len(ts[j].Values)
for _, tmp := range templates {
precedence := tmp.Precedence()
if precedence > maxPrecedence {
maxPrecedence = precedence
}
tvarsByPrecedence[precedence] = append(tvarsByPrecedence[precedence], tmp)
}
replaced := query
for prc := uint(0); prc <= maxPrecedence; prc++ {
replacements := []string{}
for _, v := range tvarsByPrecedence[prc] {
if evar, ok := v.(chronograf.ExecutableVar); ok {
evar.Exec(replaced)
}
newVal := v.String()
if newVal != "" {
replacements = append(replacements, v.Name(), newVal)
}
} }
replacer := strings.NewReplacer(replacements...) if len(ts[i].Values) == 0 {
replaced = replacer.Replace(replaced) return i < j
} }
return replaced for k := range ts[i].Values {
if ts[i].Values[k].Type != ts[j].Values[k].Type {
return ts[i].Values[k].Type < ts[j].Values[k].Type
}
if ts[i].Values[k].Value != ts[j].Values[k].Value {
return ts[i].Values[k].Value < ts[j].Values[k].Value
}
}
return i < j
})
return ts
}
// RenderTemplate converts the template variable into a correct InfluxQL string based
// on its type
func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (string, error) {
if len(t.Values) == 0 {
return query, nil
}
switch t.Values[0].Type {
case "tagKey", "fieldKey", "measurement", "database":
return strings.Replace(query, t.Var, `"`+t.Values[0].Value+`"`, -1), nil
case "tagValue", "timeStamp":
return strings.Replace(query, t.Var, `'`+t.Values[0].Value+`'`, -1), nil
case "csv", "constant":
return strings.Replace(query, t.Var, t.Values[0].Value, -1), nil
}
tv := map[string]string{}
for i := range t.Values {
tv[t.Values[i].Type] = t.Values[i].Value
}
if res, ok := tv["resolution"]; ok {
resolution, err := strconv.ParseInt(res, 0, 64)
if err != nil {
return "", err
}
ppp, ok := tv["pointsPerPixel"]
if !ok {
ppp = "3"
}
pixelsPerPoint, err := strconv.ParseInt(ppp, 0, 64)
if err != nil {
return "", err
}
dur, err := ParseTime(query, now)
if err != nil {
return "", err
}
interval := AutoGroupBy(resolution, pixelsPerPoint, dur)
return strings.Replace(query, t.Var, interval, -1), nil
}
return query, nil
}
func AutoGroupBy(resolution, pixelsPerPoint int64, duration time.Duration) string {
// The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3
// Number of points given the pixels
pixels := float64(resolution) / float64(pixelsPerPoint)
msPerPixel := float64(duration/time.Millisecond) / pixels
secPerPixel := float64(duration/time.Second) / pixels
if secPerPixel < 1.0 {
if msPerPixel < 1.0 {
msPerPixel = 1.0
}
return "time(" + strconv.FormatInt(int64(msPerPixel), 10) + "ms)"
}
// If groupby is more than 1 second round to the second
return "time(" + strconv.FormatInt(int64(secPerPixel), 10) + "s)"
}
// TemplateReplace replaces templates with values within the query string
func TemplateReplace(query string, templates []chronograf.TemplateVar, now time.Time) (string, error) {
templates = SortTemplates(templates)
for i := range templates {
var err error
query, err = RenderTemplate(query, templates[i], now)
if err != nil {
return "", err
}
}
return query, nil
} }

View File

@ -2,6 +2,7 @@ package influx
import ( import (
"encoding/json" "encoding/json"
"fmt"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -13,43 +14,43 @@ func TestTemplateReplace(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
query string query string
vars chronograf.TemplateVars vars []chronograf.TemplateVar
want string want string
}{ }{
{ {
name: "select with parameters", name: "select with parameters",
query: "$METHOD field1, $field FROM $measurement WHERE temperature > $temperature", query: ":method: field1, :field: FROM :measurement: WHERE temperature > :temperature:",
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$temperature", Var: ":temperature:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "csv", Type: "csv",
Value: "10", Value: "10",
}, },
}, },
}, },
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$field", Var: ":field:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "fieldKey", Type: "fieldKey",
Value: "field2", Value: "field2",
}, },
}, },
}, },
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$METHOD", Var: ":method:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "csv", Type: "csv",
Value: "SELECT", Value: "SELECT",
}, },
}, },
}, },
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$measurement", Var: ":measurement:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "csv", Type: "csv",
Value: `"cpu"`, Value: `"cpu"`,
@ -62,28 +63,28 @@ func TestTemplateReplace(t *testing.T) {
{ {
name: "select with parameters and aggregates", name: "select with parameters and aggregates",
query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`, query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$value", Var: "$value",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "tagValue", Type: "tagValue",
Value: "howdy.com", Value: "howdy.com",
}, },
}, },
}, },
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$tag", Var: "$tag",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "tagKey", Type: "tagKey",
Value: "host", Value: "host",
}, },
}, },
}, },
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$field", Var: "$field",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "fieldKey", Type: "fieldKey",
Value: "field", Value: "field",
@ -101,8 +102,8 @@ func TestTemplateReplace(t *testing.T) {
{ {
name: "var without a value", name: "var without a value",
query: `SELECT $field FROM "cpu"`, query: `SELECT $field FROM "cpu"`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$field", Var: "$field",
}, },
}, },
@ -111,10 +112,10 @@ func TestTemplateReplace(t *testing.T) {
{ {
name: "var with unknown type", name: "var with unknown type",
query: `SELECT $field FROM "cpu"`, query: `SELECT $field FROM "cpu"`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
chronograf.BasicTemplateVar{ chronograf.TemplateVar{
Var: "$field", Var: "$field",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "who knows?", Type: "who knows?",
Value: "field", Value: "field",
@ -127,42 +128,63 @@ func TestTemplateReplace(t *testing.T) {
{ {
name: "auto group by", name: "auto group by",
query: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by :interval:`, query: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by :interval:`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
&chronograf.GroupByVar{ {
Var: ":interval:", Var: ":interval:",
Duration: 180 * 24 * time.Hour, Values: []chronograf.TemplateValue{
Resolution: 1000, {
ReportingInterval: 10 * time.Second, Value: "1000",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
}, },
}, },
want: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(46656s)`, want: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(46655s)`,
}, },
{ {
name: "auto group by without duration", name: "auto group by without duration",
query: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by :interval:`, query: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by :interval:`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
&chronograf.GroupByVar{ {
Var: ":interval:", Var: ":interval:",
Duration: 0 * time.Minute, Values: []chronograf.TemplateValue{
Resolution: 1000, {
ReportingInterval: 10 * time.Second, Value: "1000",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
}, },
}, },
want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46656s)`, want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46655s)`,
}, },
{ {
name: "auto group by with :dashboardTime:", name: "auto group by with :dashboardTime:",
query: `SELECT mean(usage_idle) from "cpu" WHERE time > :dashboardTime: group by :interval:`, query: `SELECT mean(usage_idle) from "cpu" WHERE time > :dashboardTime: group by :interval:`,
vars: chronograf.TemplateVars{ vars: []chronograf.TemplateVar{
&chronograf.GroupByVar{ {
Var: ":interval:", Var: ":interval:",
Duration: 0 * time.Minute, Values: []chronograf.TemplateValue{
Resolution: 1000, {
ReportingInterval: 10 * time.Second, Value: "1000",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
}, },
&chronograf.BasicTemplateVar{ {
Var: ":dashboardTime:", Var: ":dashboardTime:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "constant", Type: "constant",
Value: "now() - 4320h", Value: "now() - 4320h",
@ -170,20 +192,28 @@ func TestTemplateReplace(t *testing.T) {
}, },
}, },
}, },
want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46656s)`, want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46655s)`,
}, },
{ {
name: "auto group by failing condition", name: "auto group by failing condition",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`, query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
vars: []chronograf.TemplateVariable{ vars: []chronograf.TemplateVar{
&chronograf.GroupByVar{ {
Var: ":interval:", Var: ":interval:",
Resolution: 115, Values: []chronograf.TemplateValue{
ReportingInterval: 10 * time.Second, {
Value: "115",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
}, },
chronograf.BasicTemplateVar{ {
Var: ":dashboardTime:", Var: ":dashboardTime:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Value: "now() - 1h", Value: "now() - 1h",
Type: "constant", Type: "constant",
@ -197,7 +227,14 @@ func TestTemplateReplace(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := TemplateReplace(tt.query, tt.vars) now, err := time.Parse(time.RFC3339, "1985-10-25T00:01:00Z")
if err != nil {
t.Fatal(err)
}
got, err := TemplateReplace(tt.query, tt.vars, now)
if err != nil {
t.Fatalf("TestParse unexpected TemplateReplace error: %v", err)
}
if got != tt.want { if got != tt.want {
t.Errorf("TestParse %s =\n%s\nwant\n%s", tt.name, got, tt.want) t.Errorf("TestParse %s =\n%s\nwant\n%s", tt.name, got, tt.want)
} }
@ -209,8 +246,20 @@ func Test_TemplateVarsUnmarshalling(t *testing.T) {
req := `[ req := `[
{ {
"tempVar": ":interval:", "tempVar": ":interval:",
"resolution": 1000, "values": [
"reportingInterval": 10 {
"value": "1000",
"type": "resolution"
},
{
"value": "3",
"type": "pointsPerPixel"
},
{
"value": "10",
"type": "reportingInterval"
}
]
}, },
{ {
"tempVar": ":cpu:", "tempVar": ":cpu:",
@ -224,15 +273,27 @@ func Test_TemplateVarsUnmarshalling(t *testing.T) {
} }
]` ]`
expected := []chronograf.TemplateVariable{ want := []chronograf.TemplateVar{
&chronograf.GroupByVar{ {
Var: ":interval:", Var: ":interval:",
Resolution: 1000, Values: []chronograf.TemplateValue{
ReportingInterval: 10 * time.Nanosecond, {
Value: "1000",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
{
Value: "10",
Type: "reportingInterval",
},
},
}, },
chronograf.BasicTemplateVar{ {
Var: ":cpu:", Var: ":cpu:",
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Value: "cpu-total", Value: "cpu-total",
Type: "tagValue", Type: "tagValue",
@ -242,65 +303,128 @@ func Test_TemplateVarsUnmarshalling(t *testing.T) {
}, },
} }
var tvars chronograf.TemplateVars var got []chronograf.TemplateVar
err := json.Unmarshal([]byte(req), &tvars) err := json.Unmarshal([]byte(req), &got)
if err != nil { if err != nil {
t.Fatal("Err unmarshaling:", err) t.Fatal("Err unmarshaling:", err)
} }
if len(tvars) != len(expected) { if !reflect.DeepEqual(got, want) {
t.Fatal("Expected", len(expected), "vars but found", len(tvars)) t.Errorf("UnmarshalJSON() = \n%#v\n want \n%#v\n", got, want)
}
if !reflect.DeepEqual(*(tvars[0].(*chronograf.GroupByVar)), *(expected[0].(*chronograf.GroupByVar))) {
t.Errorf("UnmarshalJSON() = \n%#v\n want \n%#v\n", *(tvars[0].(*chronograf.GroupByVar)), *(expected[0].(*chronograf.GroupByVar)))
}
if !reflect.DeepEqual(tvars[1].(chronograf.BasicTemplateVar), expected[1].(chronograf.BasicTemplateVar)) {
t.Errorf("UnmarshalJSON() = \n%#v\n want \n%#v\n", tvars[1].(chronograf.BasicTemplateVar), expected[1].(chronograf.BasicTemplateVar))
} }
} }
func TestGroupByVarString(t *testing.T) { func TestAutoGroupBy(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
tvar *chronograf.GroupByVar resolution int64
want string pixelsPerPoint int64
duration time.Duration
want string
}{ }{
{ {
name: "String() calculates the GROUP BY interval", name: "String() calculates the GROUP BY interval",
tvar: &chronograf.GroupByVar{ resolution: 700,
Resolution: 700, pixelsPerPoint: 3,
ReportingInterval: 10 * time.Second, duration: 24 * time.Hour,
Duration: 24 * time.Hour, want: "time(370s)",
},
want: "time(370s)",
}, },
{ {
name: "String() milliseconds if less than one second intervals", name: "String() milliseconds if less than one second intervals",
tvar: &chronograf.GroupByVar{ resolution: 100000,
Resolution: 100000, pixelsPerPoint: 3,
ReportingInterval: 10 * time.Second, duration: time.Hour,
Duration: time.Hour, want: "time(107ms)",
},
want: "time(107ms)",
}, },
{ {
name: "String() milliseconds if less than one millisecond", name: "String() milliseconds if less than one millisecond",
tvar: &chronograf.GroupByVar{ resolution: 100000,
Resolution: 100000, pixelsPerPoint: 3,
ReportingInterval: 10 * time.Second, duration: time.Second,
Duration: time.Second, want: "time(1ms)",
},
want: "time(1ms)",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := tt.tvar.String() got := AutoGroupBy(tt.resolution, tt.pixelsPerPoint, tt.duration)
if got != tt.want { if got != tt.want {
t.Errorf("TestGroupByVarString %s =\n%s\nwant\n%s", tt.name, got, tt.want) t.Errorf("TestAutoGroupBy %s =\n%s\nwant\n%s", tt.name, got, tt.want)
} }
}) })
} }
} }
func Test_RenderTemplate(t *testing.T) {
gbvTests := []struct {
name string
query string
want string
resolution uint // the screen resolution to render queries into
}{
{
name: "relative time only lower bound with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY time(259s)",
},
{
name: "relative time offset by week",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d - 7d AND time < now() - 7d GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d - 7d AND time < now() - 7d GROUP BY time(259s)",
},
{
name: "relative time with relative upper bound with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY time(179ms)",
},
{
name: "relative time with relative lower bound and now upper with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY time(259s)",
},
{
name: "absolute time with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY time(179ms)",
},
{
name: "absolute time with nano seconds and zero duraiton",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY time(1ms)",
},
}
for _, tt := range gbvTests {
t.Run(tt.name, func(t *testing.T) {
now, err := time.Parse(time.RFC3339, "1985-10-25T00:01:00Z")
if err != nil {
t.Fatal(err)
}
tvar := chronograf.TemplateVar{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: fmt.Sprintf("%d", tt.resolution),
Type: "resolution",
},
},
}
got, err := RenderTemplate(tt.query, tvar, now)
if err != nil {
t.Fatalf("unexpected error rendering template %v", err)
}
if got != tt.want {
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", tt.name, tt.want, got)
}
})
}
}
// SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h GROUP BY :interval: FILL(null);SELECT mean("numSeries") AS "mean_numSeries_shifted__1__h" FROM "_internal"."monitor"."database" WHERE time > now() - 1h - 1h AND time < now() - 1h GROUP BY :interval: FILL(null)

View File

@ -31,7 +31,6 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
cells := make([]dashboardCellResponse, len(dcells)) cells := make([]dashboardCellResponse, len(dcells))
for i, cell := range dcells { for i, cell := range dcells {
newCell := chronograf.DashboardCell{} newCell := chronograf.DashboardCell{}
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries)) newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
copy(newCell.Queries, cell.Queries) copy(newCell.Queries, cell.Queries)
@ -70,7 +69,17 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
// ValidDashboardCellRequest verifies that the dashboard cells have a query and // ValidDashboardCellRequest verifies that the dashboard cells have a query and
// have the correct axes specified // have the correct axes specified
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
if c == nil {
return fmt.Errorf("Chronograf dashboard cell was nil")
}
CorrectWidthHeight(c) CorrectWidthHeight(c)
for _, q := range c.Queries {
if err := ValidateQueryConfig(&q.QueryConfig); err != nil {
return err
}
}
MoveTimeShift(c)
return HasCorrectAxes(c) return HasCorrectAxes(c)
} }
@ -115,12 +124,22 @@ func CorrectWidthHeight(c *chronograf.DashboardCell) {
} }
} }
// MoveTimeShift moves TimeShift from the QueryConfig to the DashboardQuery
func MoveTimeShift(c *chronograf.DashboardCell) {
for i, query := range c.Queries {
query.Shifts = query.QueryConfig.Shifts
c.Queries[i] = query
}
}
// AddQueryConfig updates a cell by converting InfluxQL into queryconfigs // AddQueryConfig updates a cell by converting InfluxQL into queryconfigs
// If influxql cannot be represented by a full query config, then, the // If influxql cannot be represented by a full query config, then, the
// query config's raw text is set to the command. // query config's raw text is set to the command.
func AddQueryConfig(c *chronograf.DashboardCell) { func AddQueryConfig(c *chronograf.DashboardCell) {
for i, q := range c.Queries { for i, q := range c.Queries {
qc := ToQueryConfig(q.Command) qc := ToQueryConfig(q.Command)
qc.Shifts = append([]chronograf.TimeShift(nil), q.Shifts...)
q.Shifts = nil
q.QueryConfig = qc q.QueryConfig = qc
c.Queries[i] = q c.Queries[i] = q
} }

View File

@ -162,14 +162,14 @@ func Test_Service_DashboardCells(t *testing.T) {
http.StatusOK, http.StatusOK,
}, },
{ {
"cell axes should always be \"x\", \"y\", and \"y2\"", name: "cell axes should always be \"x\", \"y\", and \"y2\"",
&url.URL{ reqURL: &url.URL{
Path: "/chronograf/v1/dashboards/1/cells", Path: "/chronograf/v1/dashboards/1/cells",
}, },
map[string]string{ ctxParams: map[string]string{
"id": "1", "id": "1",
}, },
[]chronograf.DashboardCell{ mockResponse: []chronograf.DashboardCell{
{ {
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
X: 0, X: 0,
@ -182,7 +182,7 @@ func Test_Service_DashboardCells(t *testing.T) {
Axes: map[string]chronograf.Axis{}, Axes: map[string]chronograf.Axis{},
}, },
}, },
[]chronograf.DashboardCell{ expected: []chronograf.DashboardCell{
{ {
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
X: 0, X: 0,
@ -205,7 +205,7 @@ func Test_Service_DashboardCells(t *testing.T) {
}, },
}, },
}, },
http.StatusOK, expectedCode: http.StatusOK,
}, },
} }

View File

@ -219,6 +219,13 @@ func Test_newDashboardResponse(t *testing.T) {
{ {
Source: "/chronograf/v1/sources/1", Source: "/chronograf/v1/sources/1",
Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'", Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'",
Shifts: []chronograf.TimeShift{
{
Label: "Best Week Evar",
Unit: "d",
Quantity: "7",
},
},
}, },
}, },
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
@ -267,6 +274,13 @@ func Test_newDashboardResponse(t *testing.T) {
}, },
Tags: make(map[string][]string, 0), Tags: make(map[string][]string, 0),
AreTagsAccepted: false, AreTagsAccepted: false,
Shifts: []chronograf.TimeShift{
{
Label: "Best Week Evar",
Unit: "d",
Quantity: "7",
},
},
}, },
}, },
}, },

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -21,8 +22,8 @@ type QueryRequest struct {
// QueriesRequest converts all queries to queryConfigs with the help // QueriesRequest converts all queries to queryConfigs with the help
// of the template variables // of the template variables
type QueriesRequest struct { type QueriesRequest struct {
Queries []QueryRequest `json:"queries"` Queries []QueryRequest `json:"queries"`
TemplateVars chronograf.TemplateVars `json:"tempVars,omitempty"` TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
} }
// QueryResponse is the return result of a QueryRequest including // QueryResponse is the return result of a QueryRequest including
@ -33,7 +34,7 @@ type QueryResponse struct {
QueryConfig chronograf.QueryConfig `json:"queryConfig"` QueryConfig chronograf.QueryConfig `json:"queryConfig"`
QueryAST *queries.SelectStatement `json:"queryAST,omitempty"` QueryAST *queries.SelectStatement `json:"queryAST,omitempty"`
QueryTemplated *string `json:"queryTemplated,omitempty"` QueryTemplated *string `json:"queryTemplated,omitempty"`
TemplateVars chronograf.TemplateVars `json:"tempVars,omitempty"` TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
} }
// QueriesResponse is the response for a QueriesRequest // QueriesResponse is the response for a QueriesRequest
@ -72,12 +73,18 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
Query: q.Query, Query: q.Query,
} }
query := influx.TemplateReplace(q.Query, req.TemplateVars) query, err := influx.TemplateReplace(q.Query, req.TemplateVars, time.Now())
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
qc := ToQueryConfig(query) qc := ToQueryConfig(query)
if err := s.DefaultRP(ctx, &qc, &src); err != nil { if err := s.DefaultRP(ctx, &qc, &src); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger) Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return return
} }
qc.Shifts = []chronograf.TimeShift{}
qr.QueryConfig = qc qr.QueryConfig = qc
if stmt, err := queries.ParseSelect(query); err == nil { if stmt, err := queries.ParseSelect(query); err == nil {

View File

@ -60,7 +60,7 @@ func TestService_Queries(t *testing.T) {
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11" "id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
} }
]}`))), ]}`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"}},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]} want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"},"shifts":[]},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]}
`, `,
}, },
{ {
@ -81,7 +81,7 @@ func TestService_Queries(t *testing.T) {
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11" "id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
} }
]}`))), ]}`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null}}]} want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null,"shifts":[]}}]}
`, `,
}, },
{ {
@ -98,7 +98,7 @@ func TestService_Queries(t *testing.T) {
r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{
"queries": [ "queries": [
{ {
"query": "SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time > now() - 1m", "query": "SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time > :dashboardTime: AND time < :upperDashboardTime: GROUP BY :interval:",
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11" "id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
} }
], ],
@ -153,13 +153,20 @@ func TestService_Queries(t *testing.T) {
"id": "interval", "id": "interval",
"type": "constant", "type": "constant",
"tempVar": ":interval:", "tempVar": ":interval:",
"resolution": 1000, "values": [
"reportingInterval": 10000000000, {
"values": [] "value": "1000",
"type": "resolution"
},
{
"value": "3",
"type": "pointsPerPixel"
}
]
} }
] ]
}`))), }`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"_internal","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","range":{"upper":"","lower":"now() - 1m"}},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"_internal","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","tempVars":[{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":interval:","duration":60000000000,"resolution":1000,"reportingInterval":10000000000}]}]} want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY :interval:","range":null,"shifts":[]},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 15m AND time \u003c now() GROUP BY time(2s)","tempVars":[{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":interval:","values":[{"value":"1000","type":"resolution","selected":false},{"value":"3","type":"pointsPerPixel","selected":false}]}]}]}
`, `,
}, },
} }

View File

@ -1,6 +1,8 @@
package server package server
import ( import (
"fmt"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/influx"
) )
@ -22,3 +24,28 @@ func ToQueryConfig(query string) chronograf.QueryConfig {
Tags: make(map[string][]string, 0), Tags: make(map[string][]string, 0),
} }
} }
var validFieldTypes = map[string]bool{
"func": true,
"field": true,
"integer": true,
"number": true,
"regex": true,
"wildcard": true,
}
// ValidateQueryConfig checks any query config input
func ValidateQueryConfig(q *chronograf.QueryConfig) error {
for _, fld := range q.Fields {
invalid := fmt.Errorf(`invalid field type "%s" ; expect func, field, integer, number, regex, wildcard`, fld.Type)
if !validFieldTypes[fld.Type] {
return invalid
}
for _, arg := range fld.Args {
if !validFieldTypes[arg.Type] {
return invalid
}
}
}
return nil
}

View File

@ -0,0 +1,50 @@
package server
import (
"testing"
"github.com/influxdata/chronograf"
)
func TestValidateQueryConfig(t *testing.T) {
tests := []struct {
name string
q *chronograf.QueryConfig
wantErr bool
}{
{
name: "invalid field type",
q: &chronograf.QueryConfig{
Fields: []chronograf.Field{
{
Type: "invalid",
},
},
},
wantErr: true,
},
{
name: "invalid field args",
q: &chronograf.QueryConfig{
Fields: []chronograf.Field{
{
Type: "func",
Args: []chronograf.Field{
{
Type: "invalid",
},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateQueryConfig(tt.q); (err != nil) != tt.wantErr {
t.Errorf("ValidateQueryConfig() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -16,8 +16,8 @@ func TestValidTemplateRequest(t *testing.T) {
name: "Valid Template", name: "Valid Template",
template: &chronograf.Template{ template: &chronograf.Template{
Type: "fieldKeys", Type: "fieldKeys",
BasicTemplateVar: chronograf.BasicTemplateVar{ TemplateVar: chronograf.TemplateVar{
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "fieldKey", Type: "fieldKey",
}, },
@ -30,8 +30,8 @@ func TestValidTemplateRequest(t *testing.T) {
wantErr: true, wantErr: true,
template: &chronograf.Template{ template: &chronograf.Template{
Type: "Unknown Type", Type: "Unknown Type",
BasicTemplateVar: chronograf.BasicTemplateVar{ TemplateVar: chronograf.TemplateVar{
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "fieldKey", Type: "fieldKey",
}, },
@ -44,8 +44,8 @@ func TestValidTemplateRequest(t *testing.T) {
wantErr: true, wantErr: true,
template: &chronograf.Template{ template: &chronograf.Template{
Type: "csv", Type: "csv",
BasicTemplateVar: chronograf.BasicTemplateVar{ TemplateVar: chronograf.TemplateVar{
Values: []chronograf.BasicTemplateValue{ Values: []chronograf.TemplateValue{
{ {
Type: "unknown value", Type: "unknown value",
}, },

View File

@ -17,7 +17,7 @@
"test": "karma start", "test": "karma start",
"test:integration": "nightwatch tests --skip", "test:integration": "nightwatch tests --skip",
"test:lint": "yarn run lint; yarn run test", "test:lint": "yarn run lint; yarn run test",
"test:dev": "concurrently \"yarn run lint -- --watch\" \"yarn run test -- --no-single-run --reporters=verbose\"", "test:dev": "concurrently \"yarn run lint --watch\" \"yarn run test --no-single-run --reporters=verbose\"",
"clean": "rm -rf build", "clean": "rm -rf build",
"storybook": "node ./storybook.js", "storybook": "node ./storybook.js",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix" "prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"

View File

@ -1,7 +1,9 @@
import reducer from 'src/data_explorer/reducers/queryConfigs' import reducer from 'src/data_explorer/reducers/queryConfigs'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import { import {
fill, fill,
timeShift,
chooseTag, chooseTag,
groupByTag, groupByTag,
groupByTime, groupByTime,
@ -26,63 +28,63 @@ const fakeAddQueryAction = (panelID, queryID) => {
} }
} }
function buildInitialState(queryId, params) { function buildInitialState(queryID, params) {
return Object.assign({}, defaultQueryConfig({id: queryId}), params) return Object.assign({}, defaultQueryConfig({id: queryID}), params)
} }
describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => { describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const queryId = 123 const queryID = 123
it('can add a query', () => { it('can add a query', () => {
const state = reducer({}, fakeAddQueryAction('blah', queryId)) const state = reducer({}, fakeAddQueryAction('blah', queryID))
const actual = state[queryId] const actual = state[queryID]
const expected = defaultQueryConfig({id: queryId}) const expected = defaultQueryConfig({id: queryID})
expect(actual).to.deep.equal(expected) expect(actual).to.deep.equal(expected)
}) })
describe('choosing db, rp, and measurement', () => { describe('choosing db, rp, and measurement', () => {
let state let state
beforeEach(() => { beforeEach(() => {
state = reducer({}, fakeAddQueryAction('any', queryId)) state = reducer({}, fakeAddQueryAction('any', queryID))
}) })
it('sets the db and rp', () => { it('sets the db and rp', () => {
const newState = reducer( const newState = reducer(
state, state,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: 'telegraf', database: 'telegraf',
retentionPolicy: 'monitor', retentionPolicy: 'monitor',
}) })
) )
expect(newState[queryId].database).to.equal('telegraf') expect(newState[queryID].database).to.equal('telegraf')
expect(newState[queryId].retentionPolicy).to.equal('monitor') expect(newState[queryID].retentionPolicy).to.equal('monitor')
}) })
it('sets the measurement', () => { it('sets the measurement', () => {
const newState = reducer(state, chooseMeasurement(queryId, 'mem')) const newState = reducer(state, chooseMeasurement(queryID, 'mem'))
expect(newState[queryId].measurement).to.equal('mem') expect(newState[queryID].measurement).to.equal('mem')
}) })
}) })
describe('a query has measurements and fields', () => { describe('a query has measurements and fields', () => {
let state let state
beforeEach(() => { beforeEach(() => {
const one = reducer({}, fakeAddQueryAction('any', queryId)) const one = reducer({}, fakeAddQueryAction('any', queryID))
const two = reducer( const two = reducer(
one, one,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: '_internal', database: '_internal',
retentionPolicy: 'daily', retentionPolicy: 'daily',
}) })
) )
const three = reducer(two, chooseMeasurement(queryId, 'disk')) const three = reducer(two, chooseMeasurement(queryID, 'disk'))
state = reducer( state = reducer(
three, three,
addInitialField(queryId, { addInitialField(queryID, {
value: 'a great field', value: 'a great field',
type: 'field', type: 'field',
}) })
@ -92,91 +94,91 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('choosing a new namespace', () => { describe('choosing a new namespace', () => {
it('clears out the old measurement and fields', () => { it('clears out the old measurement and fields', () => {
// what about tags? // what about tags?
expect(state[queryId].measurement).to.equal('disk') expect(state[queryID].measurement).to.equal('disk')
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: 'newdb', database: 'newdb',
retentionPolicy: 'newrp', retentionPolicy: 'newrp',
}) })
) )
expect(newState[queryId].measurement).to.be.null expect(newState[queryID].measurement).to.be.null
expect(newState[queryId].fields.length).to.equal(0) expect(newState[queryID].fields.length).to.equal(0)
}) })
}) })
describe('choosing a new measurement', () => { describe('choosing a new measurement', () => {
it('leaves the namespace and clears out the old fields', () => { it('leaves the namespace and clears out the old fields', () => {
// what about tags? // what about tags?
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
chooseMeasurement(queryId, 'newmeasurement') chooseMeasurement(queryID, 'newmeasurement')
) )
expect(state[queryId].database).to.equal(newState[queryId].database) expect(state[queryID].database).to.equal(newState[queryID].database)
expect(state[queryId].retentionPolicy).to.equal( expect(state[queryID].retentionPolicy).to.equal(
newState[queryId].retentionPolicy newState[queryID].retentionPolicy
) )
expect(newState[queryId].fields.length).to.equal(0) expect(newState[queryID].fields.length).to.equal(0)
}) })
}) })
describe('DE_TOGGLE_FIELD', () => { describe('DE_TOGGLE_FIELD', () => {
it('can toggle multiple fields', () => { it('can toggle multiple fields', () => {
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, { toggleField(queryID, {
value: 'f2', value: 'f2',
type: 'field', type: 'field',
}) })
) )
expect(newState[queryId].fields.length).to.equal(2) expect(newState[queryID].fields.length).to.equal(2)
expect(newState[queryId].fields[1].alias).to.deep.equal('mean_f2') expect(newState[queryID].fields[1].alias).to.deep.equal('mean_f2')
expect(newState[queryId].fields[1].args).to.deep.equal([ expect(newState[queryID].fields[1].args).to.deep.equal([
{value: 'f2', type: 'field'}, {value: 'f2', type: 'field'},
]) ])
expect(newState[queryId].fields[1].value).to.deep.equal('mean') expect(newState[queryID].fields[1].value).to.deep.equal('mean')
}) })
it('applies a func to newly selected fields', () => { it('applies a func to newly selected fields', () => {
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
expect(state[queryId].fields[0].type).to.equal('func') expect(state[queryID].fields[0].type).to.equal('func')
expect(state[queryId].fields[0].value).to.equal('mean') expect(state[queryID].fields[0].value).to.equal('mean')
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, { toggleField(queryID, {
value: 'f2', value: 'f2',
type: 'field', type: 'field',
}) })
) )
expect(newState[queryId].fields[1].value).to.equal('mean') expect(newState[queryID].fields[1].value).to.equal('mean')
expect(newState[queryId].fields[1].alias).to.equal('mean_f2') expect(newState[queryID].fields[1].alias).to.equal('mean_f2')
expect(newState[queryId].fields[1].args).to.deep.equal([ expect(newState[queryID].fields[1].args).to.deep.equal([
{value: 'f2', type: 'field'}, {value: 'f2', type: 'field'},
]) ])
expect(newState[queryId].fields[1].type).to.equal('func') expect(newState[queryID].fields[1].type).to.equal('func')
}) })
it('adds the field property to query config if not found', () => { it('adds the field property to query config if not found', () => {
delete state[queryId].fields delete state[queryID].fields
expect(state[queryId].fields).to.equal(undefined) expect(state[queryID].fields).to.equal(undefined)
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, {value: 'fk1', type: 'field'}) toggleField(queryID, {value: 'fk1', type: 'field'})
) )
expect(newState[queryId].fields.length).to.equal(1) expect(newState[queryID].fields.length).to.equal(1)
}) })
}) })
}) })
@ -189,7 +191,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const f4 = {value: 'f4', type: 'field'} const f4 = {value: 'f4', type: 'field'}
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -201,7 +203,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
}, },
} }
const action = applyFuncsToField(queryId, { const action = applyFuncsToField(queryID, {
field: {value: 'f1', type: 'field'}, field: {value: 'f1', type: 'field'},
funcs: [ funcs: [
{value: 'fn3', type: 'func', args: []}, {value: 'fn3', type: 'func', args: []},
@ -211,7 +213,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].fields).to.deep.equal([ expect(nextState[queryID].fields).to.deep.equal([
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`}, {value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`}, {value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
{value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`}, {value: 'fn1', type: 'func', args: [f2], alias: `fn1_${f2.value}`},
@ -230,7 +232,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const groupBy = {time: '1m', tags: []} const groupBy = {time: '1m', tags: []}
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -239,35 +241,35 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
}, },
} }
const action = removeFuncs(queryId, fields, groupBy) const action = removeFuncs(queryID, fields, groupBy)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
const actual = nextState[queryId].fields const actual = nextState[queryID].fields
const expected = [f1, f2] const expected = [f1, f2]
expect(actual).to.eql(expected) expect(actual).to.eql(expected)
expect(nextState[queryId].groupBy.time).to.equal(null) expect(nextState[queryID].groupBy.time).to.equal(null)
}) })
}) })
describe('DE_CHOOSE_TAG', () => { describe('DE_CHOOSE_TAG', () => {
it('adds a tag key/value to the query', () => { it('adds a tag key/value to the query', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: { tags: {
k1: ['v0'], k1: ['v0'],
k2: ['foo'], k2: ['foo'],
}, },
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].tags).to.eql({ expect(nextState[queryID].tags).to.eql({
k1: ['v0', 'v1'], k1: ['v0', 'v1'],
k2: ['foo'], k2: ['foo'],
}) })
@ -275,31 +277,31 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
it("creates a new entry if it's the first key", () => { it("creates a new entry if it's the first key", () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: {}, tags: {},
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].tags).to.eql({ expect(nextState[queryID].tags).to.eql({
k1: ['v1'], k1: ['v1'],
}) })
}) })
it('removes a value that is already in the list', () => { it('removes a value that is already in the list', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: { tags: {
k1: ['v1'], k1: ['v1'],
}, },
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
@ -307,14 +309,14 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
// TODO: this should probably remove the `k1` property entirely from the tags object // TODO: this should probably remove the `k1` property entirely from the tags object
expect(nextState[queryId].tags).to.eql({}) expect(nextState[queryID].tags).to.eql({})
}) })
}) })
describe('DE_GROUP_BY_TAG', () => { describe('DE_GROUP_BY_TAG', () => {
it('adds a tag key/value to the query', () => { it('adds a tag key/value to the query', () => {
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -323,11 +325,11 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
groupBy: {tags: [], time: null}, groupBy: {tags: [], time: null},
}, },
} }
const action = groupByTag(queryId, 'k1') const action = groupByTag(queryID, 'k1')
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy).to.eql({ expect(nextState[queryID].groupBy).to.eql({
time: null, time: null,
tags: ['k1'], tags: ['k1'],
}) })
@ -335,7 +337,7 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
it('removes a tag if the given tag key is already in the GROUP BY list', () => { it('removes a tag if the given tag key is already in the GROUP BY list', () => {
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -344,11 +346,11 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
groupBy: {tags: ['k1'], time: null}, groupBy: {tags: ['k1'], time: null},
}, },
} }
const action = groupByTag(queryId, 'k1') const action = groupByTag(queryID, 'k1')
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy).to.eql({ expect(nextState[queryID].groupBy).to.eql({
time: null, time: null,
tags: [], tags: [],
}) })
@ -358,14 +360,14 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
describe('DE_TOGGLE_TAG_ACCEPTANCE', () => { describe('DE_TOGGLE_TAG_ACCEPTANCE', () => {
it('it toggles areTagsAccepted', () => { it('it toggles areTagsAccepted', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const action = toggleTagAcceptance(queryId) const action = toggleTagAcceptance(queryID)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].areTagsAccepted).to.equal( expect(nextState[queryID].areTagsAccepted).to.equal(
!initialState[queryId].areTagsAccepted !initialState[queryID].areTagsAccepted
) )
}) })
}) })
@ -374,99 +376,113 @@ describe('Chronograf.Reducers.DataExplorer.queryConfigs', () => {
it('applys the appropriate group by time', () => { it('applys the appropriate group by time', () => {
const time = '100y' const time = '100y'
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const action = groupByTime(queryId, time) const action = groupByTime(queryID, time)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy.time).to.equal(time) expect(nextState[queryID].groupBy.time).to.equal(time)
}) })
}) })
it('updates entire config', () => { it('updates entire config', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const expected = defaultQueryConfig({id: queryId}, {rawText: 'hello'}) const expected = defaultQueryConfig({id: queryID}, {rawText: 'hello'})
const action = updateQueryConfig(expected) const action = updateQueryConfig(expected)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId]).to.deep.equal(expected) expect(nextState[queryID]).to.deep.equal(expected)
}) })
it("updates a query's raw text", () => { it("updates a query's raw text", () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const text = 'foo' const text = 'foo'
const action = updateRawQuery(queryId, text) const action = updateRawQuery(queryID, text)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].rawText).to.equal('foo') expect(nextState[queryID].rawText).to.equal('foo')
}) })
it("updates a query's raw status", () => { it("updates a query's raw status", () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const status = 'your query was sweet' const status = 'your query was sweet'
const action = editQueryStatus(queryId, status) const action = editQueryStatus(queryID, status)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].status).to.equal(status) expect(nextState[queryID].status).to.equal(status)
}) })
describe('DE_FILL', () => { describe('DE_FILL', () => {
it('applies an explicit fill when group by time is used', () => { it('applies an explicit fill when group by time is used', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const time = '10s' const time = '10s'
const action = groupByTime(queryId, time) const action = groupByTime(queryID, time)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].fill).to.equal(NULL_STRING) expect(nextState[queryID].fill).to.equal(NULL_STRING)
}) })
it('updates fill to non-null-string non-number string value', () => { it('updates fill to non-null-string non-number string value', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const action = fill(queryId, LINEAR) const action = fill(queryID, LINEAR)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].fill).to.equal(LINEAR) expect(nextState[queryID].fill).to.equal(LINEAR)
}) })
it('updates fill to string integer value', () => { it('updates fill to string integer value', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const INT_STRING = '1337' const INT_STRING = '1337'
const action = fill(queryId, INT_STRING) const action = fill(queryID, INT_STRING)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].fill).to.equal(INT_STRING) expect(nextState[queryID].fill).to.equal(INT_STRING)
}) })
it('updates fill to string float value', () => { it('updates fill to string float value', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const FLOAT_STRING = '1.337' const FLOAT_STRING = '1.337'
const action = fill(queryId, FLOAT_STRING) const action = fill(queryID, FLOAT_STRING)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].fill).to.equal(FLOAT_STRING) expect(nextState[queryID].fill).to.equal(FLOAT_STRING)
})
})
describe('DE_TIME_SHIFT', () => {
it('can shift the time', () => {
const initialState = {
[queryID]: buildInitialState(queryID),
}
const shift = {quantity: 1, unit: 'd', duration: '1d'}
const action = timeShift(queryID, shift)
const nextState = reducer(initialState, action)
expect(nextState[queryID].shifts).to.deep.equal([shift])
}) })
}) })
}) })

View File

@ -1,14 +1,15 @@
import reducer from 'src/kapacitor/reducers/queryConfigs' import reducer from 'src/kapacitor/reducers/queryConfigs'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import { import {
chooseTag,
timeShift,
groupByTag,
toggleField,
groupByTime,
chooseNamespace, chooseNamespace,
chooseMeasurement, chooseMeasurement,
chooseTag,
groupByTag,
toggleTagAcceptance,
toggleField,
applyFuncsToField, applyFuncsToField,
groupByTime, toggleTagAcceptance,
} from 'src/kapacitor/actions/queryConfigs' } from 'src/kapacitor/actions/queryConfigs'
const fakeAddQueryAction = (panelID, queryID) => { const fakeAddQueryAction = (panelID, queryID) => {
@ -18,142 +19,142 @@ const fakeAddQueryAction = (panelID, queryID) => {
} }
} }
function buildInitialState(queryId, params) { function buildInitialState(queryID, params) {
return Object.assign( return Object.assign(
{}, {},
defaultQueryConfig({id: queryId, isKapacitorRule: true}), defaultQueryConfig({id: queryID, isKapacitorRule: true}),
params params
) )
} }
describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => { describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
const queryId = 123 const queryID = 123
it('can add a query', () => { it('can add a query', () => {
const state = reducer({}, fakeAddQueryAction('blah', queryId)) const state = reducer({}, fakeAddQueryAction('blah', queryID))
const actual = state[queryId] const actual = state[queryID]
const expected = defaultQueryConfig({id: queryId, isKapacitorRule: true}) const expected = defaultQueryConfig({id: queryID, isKapacitorRule: true})
expect(actual).to.deep.equal(expected) expect(actual).to.deep.equal(expected)
}) })
describe('choosing db, rp, and measurement', () => { describe('choosing db, rp, and measurement', () => {
let state let state
beforeEach(() => { beforeEach(() => {
state = reducer({}, fakeAddQueryAction('any', queryId)) state = reducer({}, fakeAddQueryAction('any', queryID))
}) })
it('sets the db and rp', () => { it('sets the db and rp', () => {
const newState = reducer( const newState = reducer(
state, state,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: 'telegraf', database: 'telegraf',
retentionPolicy: 'monitor', retentionPolicy: 'monitor',
}) })
) )
expect(newState[queryId].database).to.equal('telegraf') expect(newState[queryID].database).to.equal('telegraf')
expect(newState[queryId].retentionPolicy).to.equal('monitor') expect(newState[queryID].retentionPolicy).to.equal('monitor')
}) })
it('sets the measurement', () => { it('sets the measurement', () => {
const newState = reducer(state, chooseMeasurement(queryId, 'mem')) const newState = reducer(state, chooseMeasurement(queryID, 'mem'))
expect(newState[queryId].measurement).to.equal('mem') expect(newState[queryID].measurement).to.equal('mem')
}) })
}) })
describe('a query has measurements and fields', () => { describe('a query has measurements and fields', () => {
let state let state
beforeEach(() => { beforeEach(() => {
const one = reducer({}, fakeAddQueryAction('any', queryId)) const one = reducer({}, fakeAddQueryAction('any', queryID))
const two = reducer( const two = reducer(
one, one,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: '_internal', database: '_internal',
retentionPolicy: 'daily', retentionPolicy: 'daily',
}) })
) )
const three = reducer(two, chooseMeasurement(queryId, 'disk')) const three = reducer(two, chooseMeasurement(queryID, 'disk'))
state = reducer( state = reducer(
three, three,
toggleField(queryId, {value: 'a great field', funcs: []}) toggleField(queryID, {value: 'a great field', funcs: []})
) )
}) })
describe('choosing a new namespace', () => { describe('choosing a new namespace', () => {
it('clears out the old measurement and fields', () => { it('clears out the old measurement and fields', () => {
// what about tags? // what about tags?
expect(state[queryId].measurement).to.exist expect(state[queryID].measurement).to.exist
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
chooseNamespace(queryId, { chooseNamespace(queryID, {
database: 'newdb', database: 'newdb',
retentionPolicy: 'newrp', retentionPolicy: 'newrp',
}) })
) )
expect(newState[queryId].measurement).not.to.exist expect(newState[queryID].measurement).not.to.exist
expect(newState[queryId].fields.length).to.equal(0) expect(newState[queryID].fields.length).to.equal(0)
}) })
}) })
describe('choosing a new measurement', () => { describe('choosing a new measurement', () => {
it('leaves the namespace and clears out the old fields', () => { it('leaves the namespace and clears out the old fields', () => {
// what about tags? // what about tags?
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
chooseMeasurement(queryId, 'newmeasurement') chooseMeasurement(queryID, 'newmeasurement')
) )
expect(state[queryId].database).to.equal(newState[queryId].database) expect(state[queryID].database).to.equal(newState[queryID].database)
expect(state[queryId].retentionPolicy).to.equal( expect(state[queryID].retentionPolicy).to.equal(
newState[queryId].retentionPolicy newState[queryID].retentionPolicy
) )
expect(newState[queryId].fields.length).to.equal(0) expect(newState[queryID].fields.length).to.equal(0)
}) })
}) })
describe('when the query is part of a kapacitor rule', () => { describe('when the query is part of a kapacitor rule', () => {
it('only allows one field', () => { it('only allows one field', () => {
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, {value: 'a different field', type: 'field'}) toggleField(queryID, {value: 'a different field', type: 'field'})
) )
expect(newState[queryId].fields.length).to.equal(1) expect(newState[queryID].fields.length).to.equal(1)
expect(newState[queryId].fields[0].value).to.equal('a different field') expect(newState[queryID].fields[0].value).to.equal('a different field')
}) })
}) })
describe('KAPA_TOGGLE_FIELD', () => { describe('KAPA_TOGGLE_FIELD', () => {
it('cannot toggle multiple fields', () => { it('cannot toggle multiple fields', () => {
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, {value: 'a different field', type: 'field'}) toggleField(queryID, {value: 'a different field', type: 'field'})
) )
expect(newState[queryId].fields.length).to.equal(1) expect(newState[queryID].fields.length).to.equal(1)
expect(newState[queryId].fields[0].value).to.equal('a different field') expect(newState[queryID].fields[0].value).to.equal('a different field')
}) })
it('applies no funcs to newly selected fields', () => { it('applies no funcs to newly selected fields', () => {
expect(state[queryId].fields.length).to.equal(1) expect(state[queryID].fields.length).to.equal(1)
const newState = reducer( const newState = reducer(
state, state,
toggleField(queryId, {value: 'a different field', type: 'field'}) toggleField(queryID, {value: 'a different field', type: 'field'})
) )
expect(newState[queryId].fields[0].type).to.equal('field') expect(newState[queryID].fields[0].type).to.equal('field')
}) })
}) })
}) })
@ -162,7 +163,7 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
it('applies functions to a field without any existing functions', () => { it('applies functions to a field without any existing functions', () => {
const f1 = {value: 'f1', type: 'field'} const f1 = {value: 'f1', type: 'field'}
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -174,13 +175,13 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
}, },
} }
const action = applyFuncsToField(queryId, { const action = applyFuncsToField(queryID, {
field: {value: 'f1', type: 'field'}, field: {value: 'f1', type: 'field'},
funcs: [{value: 'fn3', type: 'func'}, {value: 'fn4', type: 'func'}], funcs: [{value: 'fn3', type: 'func'}, {value: 'fn4', type: 'func'}],
}) })
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
const actual = nextState[queryId].fields const actual = nextState[queryID].fields
const expected = [ const expected = [
{value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`}, {value: 'fn3', type: 'func', args: [f1], alias: `fn3_${f1.value}`},
{value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`}, {value: 'fn4', type: 'func', args: [f1], alias: `fn4_${f1.value}`},
@ -193,21 +194,21 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
describe('KAPA_CHOOSE_TAG', () => { describe('KAPA_CHOOSE_TAG', () => {
it('adds a tag key/value to the query', () => { it('adds a tag key/value to the query', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: { tags: {
k1: ['v0'], k1: ['v0'],
k2: ['foo'], k2: ['foo'],
}, },
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].tags).to.eql({ expect(nextState[queryID].tags).to.eql({
k1: ['v0', 'v1'], k1: ['v0', 'v1'],
k2: ['foo'], k2: ['foo'],
}) })
@ -215,31 +216,31 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
it("creates a new entry if it's the first key", () => { it("creates a new entry if it's the first key", () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: {}, tags: {},
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].tags).to.eql({ expect(nextState[queryID].tags).to.eql({
k1: ['v1'], k1: ['v1'],
}) })
}) })
it('removes a value that is already in the list', () => { it('removes a value that is already in the list', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId, { [queryID]: buildInitialState(queryID, {
tags: { tags: {
k1: ['v1'], k1: ['v1'],
}, },
}), }),
} }
const action = chooseTag(queryId, { const action = chooseTag(queryID, {
key: 'k1', key: 'k1',
value: 'v1', value: 'v1',
}) })
@ -247,14 +248,14 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
// TODO: this should probably remove the `k1` property entirely from the tags object // TODO: this should probably remove the `k1` property entirely from the tags object
expect(nextState[queryId].tags).to.eql({}) expect(nextState[queryID].tags).to.eql({})
}) })
}) })
describe('KAPA_GROUP_BY_TAG', () => { describe('KAPA_GROUP_BY_TAG', () => {
it('adds a tag key/value to the query', () => { it('adds a tag key/value to the query', () => {
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -263,11 +264,11 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
groupBy: {tags: [], time: null}, groupBy: {tags: [], time: null},
}, },
} }
const action = groupByTag(queryId, 'k1') const action = groupByTag(queryID, 'k1')
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy).to.eql({ expect(nextState[queryID].groupBy).to.eql({
time: null, time: null,
tags: ['k1'], tags: ['k1'],
}) })
@ -275,7 +276,7 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
it('removes a tag if the given tag key is already in the GROUP BY list', () => { it('removes a tag if the given tag key is already in the GROUP BY list', () => {
const initialState = { const initialState = {
[queryId]: { [queryID]: {
id: 123, id: 123,
database: 'db1', database: 'db1',
measurement: 'm1', measurement: 'm1',
@ -284,11 +285,11 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
groupBy: {tags: ['k1'], time: null}, groupBy: {tags: ['k1'], time: null},
}, },
} }
const action = groupByTag(queryId, 'k1') const action = groupByTag(queryID, 'k1')
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy).to.eql({ expect(nextState[queryID].groupBy).to.eql({
time: null, time: null,
tags: [], tags: [],
}) })
@ -298,14 +299,14 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
describe('KAPA_TOGGLE_TAG_ACCEPTANCE', () => { describe('KAPA_TOGGLE_TAG_ACCEPTANCE', () => {
it('it toggles areTagsAccepted', () => { it('it toggles areTagsAccepted', () => {
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const action = toggleTagAcceptance(queryId) const action = toggleTagAcceptance(queryID)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].areTagsAccepted).to.equal( expect(nextState[queryID].areTagsAccepted).to.equal(
!initialState[queryId].areTagsAccepted !initialState[queryID].areTagsAccepted
) )
}) })
}) })
@ -314,14 +315,28 @@ describe('Chronograf.Reducers.Kapacitor.queryConfigs', () => {
it('applys the appropriate group by time', () => { it('applys the appropriate group by time', () => {
const time = '100y' const time = '100y'
const initialState = { const initialState = {
[queryId]: buildInitialState(queryId), [queryID]: buildInitialState(queryID),
} }
const action = groupByTime(queryId, time) const action = groupByTime(queryID, time)
const nextState = reducer(initialState, action) const nextState = reducer(initialState, action)
expect(nextState[queryId].groupBy.time).to.equal(time) expect(nextState[queryID].groupBy.time).to.equal(time)
})
})
describe('KAPA_TIME_SHIFT', () => {
it('can shift the time', () => {
const initialState = {
[queryID]: buildInitialState(queryID),
}
const shift = {quantity: 1, unit: 'd', duration: '1d'}
const action = timeShift(queryID, shift)
const nextState = reducer(initialState, action)
expect(nextState[queryID].shifts).to.deep.equal([shift])
}) })
}) })
}) })

View File

@ -0,0 +1,109 @@
import {timeRangeType, shiftTimeRange} from 'shared/query/helpers'
import moment from 'moment'
import {
INVALID,
ABSOLUTE,
INFLUXQL,
RELATIVE_LOWER,
RELATIVE_UPPER,
} from 'shared/constants/timeRange'
const format = INFLUXQL
describe('Shared.Query.Helpers', () => {
describe('timeRangeTypes', () => {
it('returns invalid if no upper and lower', () => {
const upper = null
const lower = null
const timeRange = {lower, upper}
expect(timeRangeType(timeRange)).to.equal(INVALID)
})
it('can detect absolute type', () => {
const tenMinutes = 600000
const upper = Date.now()
const lower = upper - tenMinutes
const timeRange = {lower, upper, format}
expect(timeRangeType(timeRange)).to.equal(ABSOLUTE)
})
it('can detect exclusive relative lower', () => {
const lower = 'now() - 15m'
const upper = null
const timeRange = {lower, upper, format}
expect(timeRangeType(timeRange)).to.equal(RELATIVE_LOWER)
})
it('can detect relative upper', () => {
const upper = 'now()'
const oneMinute = 60000
const lower = Date.now() - oneMinute
const timeRange = {lower, upper, format}
expect(timeRangeType(timeRange)).to.equal(RELATIVE_UPPER)
})
})
describe('timeRangeShift', () => {
it('can calculate the shift for absolute timeRanges', () => {
const upper = Date.now()
const oneMinute = 60000
const lower = Date.now() - oneMinute
const shift = {quantity: 7, unit: 'd'}
const timeRange = {upper, lower}
const type = timeRangeType(timeRange)
const actual = shiftTimeRange(timeRange, shift)
const expected = {
lower: `${lower} - 7d`,
upper: `${upper} - 7d`,
type: 'shifted',
}
expect(type).to.equal(ABSOLUTE)
expect(actual).to.deep.equal(expected)
})
it('can calculate the shift for relative lower timeRanges', () => {
const shift = {quantity: 7, unit: 'd'}
const lower = 'now() - 15m'
const timeRange = {lower, upper: null}
const type = timeRangeType(timeRange)
const actual = shiftTimeRange(timeRange, shift)
const expected = {
lower: `${lower} - 7d`,
upper: `now() - 7d`,
type: 'shifted',
}
expect(type).to.equal(RELATIVE_LOWER)
expect(actual).to.deep.equal(expected)
})
it('can calculate the shift for relative upper timeRanges', () => {
const upper = Date.now()
const oneMinute = 60000
const lower = Date.now() - oneMinute
const shift = {quantity: 7, unit: 'd'}
const timeRange = {upper, lower}
const type = timeRangeType(timeRange)
const actual = shiftTimeRange(timeRange, shift)
const expected = {
lower: `${lower} - 7d`,
upper: `${upper} - 7d`,
type: 'shifted',
}
expect(type).to.equal(ABSOLUTE)
expect(actual).to.deep.equal(expected)
})
})
})

View File

@ -228,11 +228,7 @@ describe('timeSeriesToDygraph', () => {
] ]
const isInDataExplorer = true const isInDataExplorer = true
const actual = timeSeriesToDygraph( const actual = timeSeriesToDygraph(influxResponse, isInDataExplorer)
influxResponse,
undefined,
isInDataExplorer
)
const expected = {} const expected = {}

View File

@ -79,35 +79,29 @@ export const applyMasks = query => {
const maskForWholeTemplates = '😸$1😸' const maskForWholeTemplates = '😸$1😸'
return query.replace(matchWholeTemplates, maskForWholeTemplates) return query.replace(matchWholeTemplates, maskForWholeTemplates)
} }
export const insertTempVar = (query, tempVar) => { export const insertTempVar = (query, tempVar) => {
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar) return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
} }
export const unMask = query => { export const unMask = query => {
return query.replace(/😸/g, ':') return query.replace(/😸/g, ':')
} }
export const removeUnselectedTemplateValues = templates => { export const removeUnselectedTemplateValues = templates => {
return templates.map(template => { return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected) const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues} return {...template, values: selectedValues}
}) })
} }
export const DISPLAY_OPTIONS = { export const DISPLAY_OPTIONS = {
LINEAR: 'linear', LINEAR: 'linear',
LOG: 'log', LOG: 'log',
BASE_2: '2', BASE_2: '2',
BASE_10: '10', BASE_10: '10',
} }
export const TOOLTIP_CONTENT = { export const TOOLTIP_CONTENT = {
FORMAT: FORMAT:
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>', '<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
} }
export const TYPE_QUERY_CONFIG = 'queryConfig' export const TYPE_QUERY_CONFIG = 'queryConfig'
export const TYPE_SHIFTED = 'shifted queryConfig'
export const TYPE_IFQL = 'ifql' export const TYPE_IFQL = 'ifql'
export const DASHBOARD_NAME_MAX_LENGTH = 50 export const DASHBOARD_NAME_MAX_LENGTH = 50

View File

@ -39,13 +39,12 @@ class DashboardPage extends Component {
selectedCell: null, selectedCell: null,
isTemplating: false, isTemplating: false,
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null}, zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
names: [],
} }
} }
async componentDidMount() { async componentDidMount() {
const { const {
params: {dashboardID, sourceID}, params: {dashboardID},
dashboardActions: { dashboardActions: {
getDashboardsAsync, getDashboardsAsync,
updateTempVarValues, updateTempVarValues,
@ -62,13 +61,6 @@ class DashboardPage extends Component {
// Refresh and persists influxql generated template variable values // Refresh and persists influxql generated template variable values
await updateTempVarValues(source, dashboard) await updateTempVarValues(source, dashboard)
await putDashboardByID(dashboardID) await putDashboardByID(dashboardID)
const names = dashboards.map(d => ({
name: d.name,
link: `/sources/${sourceID}/dashboards/${d.id}`,
}))
this.setState({names})
} }
handleOpenTemplateManager = () => { handleOpenTemplateManager = () => {
@ -263,14 +255,23 @@ class DashboardPage extends Component {
], ],
} }
// this controls the auto group by behavior
const interval = { const interval = {
id: 'interval', id: 'interval',
type: 'constant', type: 'autoGroupBy',
tempVar: ':interval:', tempVar: ':interval:',
resolution: 1000, label: 'automatically determine the best group by time',
reportingInterval: 10000000000, values: [
values: [], {
value: '1000', // pixels
type: 'resolution',
selected: true,
},
{
value: '3',
type: 'pointsPerPixel',
selected: true,
},
],
} }
let templatesIncludingDashTime let templatesIncludingDashTime
@ -285,7 +286,11 @@ class DashboardPage extends Component {
templatesIncludingDashTime = [] templatesIncludingDashTime = []
} }
const {selectedCell, isEditMode, isTemplating, names} = this.state const {selectedCell, isEditMode, isTemplating} = this.state
const names = dashboards.map(d => ({
name: d.name,
link: `/sources/${sourceID}/dashboards/${d.id}`,
}))
return ( return (
<div className="page"> <div className="page">

View File

@ -18,26 +18,26 @@ export const deleteQuery = queryID => ({
}, },
}) })
export const toggleField = (queryId, fieldFunc) => ({ export const toggleField = (queryID, fieldFunc) => ({
type: 'DE_TOGGLE_FIELD', type: 'DE_TOGGLE_FIELD',
payload: { payload: {
queryId, queryID,
fieldFunc, fieldFunc,
}, },
}) })
export const groupByTime = (queryId, time) => ({ export const groupByTime = (queryID, time) => ({
type: 'DE_GROUP_BY_TIME', type: 'DE_GROUP_BY_TIME',
payload: { payload: {
queryId, queryID,
time, time,
}, },
}) })
export const fill = (queryId, value) => ({ export const fill = (queryID, value) => ({
type: 'DE_FILL', type: 'DE_FILL',
payload: { payload: {
queryId, queryID,
value, value,
}, },
}) })
@ -51,44 +51,44 @@ export const removeFuncs = (queryID, fields, groupBy) => ({
}, },
}) })
export const applyFuncsToField = (queryId, fieldFunc, groupBy) => ({ export const applyFuncsToField = (queryID, fieldFunc, groupBy) => ({
type: 'DE_APPLY_FUNCS_TO_FIELD', type: 'DE_APPLY_FUNCS_TO_FIELD',
payload: { payload: {
queryId, queryID,
fieldFunc, fieldFunc,
groupBy, groupBy,
}, },
}) })
export const chooseTag = (queryId, tag) => ({ export const chooseTag = (queryID, tag) => ({
type: 'DE_CHOOSE_TAG', type: 'DE_CHOOSE_TAG',
payload: { payload: {
queryId, queryID,
tag, tag,
}, },
}) })
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({ export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
type: 'DE_CHOOSE_NAMESPACE', type: 'DE_CHOOSE_NAMESPACE',
payload: { payload: {
queryId, queryID,
database, database,
retentionPolicy, retentionPolicy,
}, },
}) })
export const chooseMeasurement = (queryId, measurement) => ({ export const chooseMeasurement = (queryID, measurement) => ({
type: 'DE_CHOOSE_MEASUREMENT', type: 'DE_CHOOSE_MEASUREMENT',
payload: { payload: {
queryId, queryID,
measurement, measurement,
}, },
}) })
export const editRawText = (queryId, rawText) => ({ export const editRawText = (queryID, rawText) => ({
type: 'DE_EDIT_RAW_TEXT', type: 'DE_EDIT_RAW_TEXT',
payload: { payload: {
queryId, queryID,
rawText, rawText,
}, },
}) })
@ -100,18 +100,18 @@ export const setTimeRange = bounds => ({
}, },
}) })
export const groupByTag = (queryId, tagKey) => ({ export const groupByTag = (queryID, tagKey) => ({
type: 'DE_GROUP_BY_TAG', type: 'DE_GROUP_BY_TAG',
payload: { payload: {
queryId, queryID,
tagKey, tagKey,
}, },
}) })
export const toggleTagAcceptance = queryId => ({ export const toggleTagAcceptance = queryID => ({
type: 'DE_TOGGLE_TAG_ACCEPTANCE', type: 'DE_TOGGLE_TAG_ACCEPTANCE',
payload: { payload: {
queryId, queryID,
}, },
}) })
@ -147,6 +147,14 @@ export const editQueryStatus = (queryID, status) => ({
}, },
}) })
export const timeShift = (queryID, shift) => ({
type: 'DE_TIME_SHIFT',
payload: {
queryID,
shift,
},
})
// Async actions // Async actions
export const editRawTextAsync = (url, id, text) => async dispatch => { export const editRawTextAsync = (url, id, text) => async dispatch => {
try { try {

View File

@ -7,8 +7,6 @@ import Dropdown from 'shared/components/Dropdown'
import {AUTO_GROUP_BY} from 'shared/constants' import {AUTO_GROUP_BY} from 'shared/constants'
const {func, string, shape} = PropTypes
const isInRuleBuilder = pathname => pathname.includes('alert-rules') const isInRuleBuilder = pathname => pathname.includes('alert-rules')
const isInDataExplorer = pathname => pathname.includes('data-explorer') const isInDataExplorer = pathname => pathname.includes('data-explorer')
@ -37,6 +35,8 @@ const GroupByTimeDropdown = ({
/> />
</div> </div>
const {func, string, shape} = PropTypes
GroupByTimeDropdown.propTypes = { GroupByTimeDropdown.propTypes = {
location: shape({ location: shape({
pathname: string.isRequired, pathname: string.isRequired,

View File

@ -88,7 +88,14 @@ class ChronoTable extends Component {
) )
} }
makeTabName = ({name, tags}) => (tags ? `${name}.${tags[name]}` : name) makeTabName = ({name, tags}) => {
if (!tags) {
return name
}
const tagKeys = Object.keys(tags).sort()
const tagValues = tagKeys.map(key => tags[key]).join('.')
return `${name}.${tagValues}`
}
render() { render() {
const {containerWidth, height, query} = this.props const {containerWidth, height, query} = this.props
@ -135,9 +142,13 @@ class ChronoTable extends Component {
</div> </div>
: <Dropdown : <Dropdown
className="dropdown-160 table--tabs-dropdown" className="dropdown-160 table--tabs-dropdown"
items={series.map((s, index) => ({...s, text: s.name, index}))} items={series.map((s, index) => ({
...s,
text: this.makeTabName(s),
index,
}))}
onChoose={this.handleClickDropdown} onChoose={this.handleClickDropdown}
selected={series[activeSeriesIndex].name} selected={this.makeTabName(series[activeSeriesIndex])}
buttonSize="btn-xs" buttonSize="btn-xs"
/>} />}
<div className="table--tabs-content"> <div className="table--tabs-content">

View File

@ -1,9 +1,9 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import buildInfluxQLQuery from 'utils/influxql'
import classnames from 'classnames' import classnames from 'classnames'
import VisHeader from 'src/data_explorer/components/VisHeader' import VisHeader from 'src/data_explorer/components/VisHeader'
import VisView from 'src/data_explorer/components/VisView' import VisView from 'src/data_explorer/components/VisView'
import {GRAPH, TABLE} from 'shared/constants' import {GRAPH, TABLE} from 'shared/constants'
import buildQueries from 'utils/buildQueriesForGraphs'
import _ from 'lodash' import _ from 'lodash'
const META_QUERY_REGEX = /^(show|create|drop)/i const META_QUERY_REGEX = /^(show|create|drop)/i
@ -61,19 +61,11 @@ class Visualization extends Component {
resizerBottomHeight, resizerBottomHeight,
errorThrown, errorThrown,
} = this.props } = this.props
const {source: {links: {proxy}}} = this.context const {source: {links: {proxy}}} = this.context
const {view} = this.state const {view} = this.state
const statements = queryConfigs.map(query => { const queries = buildQueries(proxy, queryConfigs, timeRange)
const text =
query.rawText || buildInfluxQLQuery(query.range || timeRange, query)
return {text, id: query.id, queryConfig: query}
})
const queries = statements.filter(s => s.text !== null).map(s => {
return {host: [proxy], text: s.text, id: s.id, queryConfig: s.queryConfig}
})
const activeQuery = queries[activeQueryIndex] const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0] const defaultQuery = queries[0]
const query = activeQuery || defaultQuery const query = activeQuery || defaultQuery
@ -81,12 +73,12 @@ class Visualization extends Component {
return ( return (
<div className="graph" style={{height}}> <div className="graph" style={{height}}>
<VisHeader <VisHeader
views={views}
view={view} view={view}
onToggleView={this.handleToggleView} views={views}
name={cellName}
query={query} query={query}
name={cellName}
errorThrown={errorThrown} errorThrown={errorThrown}
onToggleView={this.handleToggleView}
/> />
<div <div
className={classnames({ className={classnames({

View File

@ -3,6 +3,7 @@ import _ from 'lodash'
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import { import {
fill, fill,
timeShift,
chooseTag, chooseTag,
groupByTag, groupByTag,
removeFuncs, removeFuncs,
@ -20,24 +21,24 @@ import {
const queryConfigs = (state = {}, action) => { const queryConfigs = (state = {}, action) => {
switch (action.type) { switch (action.type) {
case 'DE_CHOOSE_NAMESPACE': { case 'DE_CHOOSE_NAMESPACE': {
const {queryId, database, retentionPolicy} = action.payload const {queryID, database, retentionPolicy} = action.payload
const nextQueryConfig = chooseNamespace(state[queryId], { const nextQueryConfig = chooseNamespace(state[queryID], {
database, database,
retentionPolicy, retentionPolicy,
}) })
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: Object.assign(nextQueryConfig, {rawText: null}), [queryID]: Object.assign(nextQueryConfig, {rawText: null}),
}) })
} }
case 'DE_CHOOSE_MEASUREMENT': { case 'DE_CHOOSE_MEASUREMENT': {
const {queryId, measurement} = action.payload const {queryID, measurement} = action.payload
const nextQueryConfig = chooseMeasurement(state[queryId], measurement) const nextQueryConfig = chooseMeasurement(state[queryID], measurement)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: Object.assign(nextQueryConfig, { [queryID]: Object.assign(nextQueryConfig, {
rawText: state[queryId].rawText, rawText: state[queryID].rawText,
}), }),
}) })
} }
@ -64,78 +65,78 @@ const queryConfigs = (state = {}, action) => {
} }
case 'DE_EDIT_RAW_TEXT': { case 'DE_EDIT_RAW_TEXT': {
const {queryId, rawText} = action.payload const {queryID, rawText} = action.payload
const nextQueryConfig = editRawText(state[queryId], rawText) const nextQueryConfig = editRawText(state[queryID], rawText)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_GROUP_BY_TIME': { case 'DE_GROUP_BY_TIME': {
const {queryId, time} = action.payload const {queryID, time} = action.payload
const nextQueryConfig = groupByTime(state[queryId], time) const nextQueryConfig = groupByTime(state[queryID], time)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_TOGGLE_TAG_ACCEPTANCE': { case 'DE_TOGGLE_TAG_ACCEPTANCE': {
const {queryId} = action.payload const {queryID} = action.payload
const nextQueryConfig = toggleTagAcceptance(state[queryId]) const nextQueryConfig = toggleTagAcceptance(state[queryID])
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_TOGGLE_FIELD': { case 'DE_TOGGLE_FIELD': {
const {queryId, fieldFunc} = action.payload const {queryID, fieldFunc} = action.payload
const nextQueryConfig = toggleField(state[queryId], fieldFunc) const nextQueryConfig = toggleField(state[queryID], fieldFunc)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: {...nextQueryConfig, rawText: null}, [queryID]: {...nextQueryConfig, rawText: null},
}) })
} }
case 'DE_APPLY_FUNCS_TO_FIELD': { case 'DE_APPLY_FUNCS_TO_FIELD': {
const {queryId, fieldFunc, groupBy} = action.payload const {queryID, fieldFunc, groupBy} = action.payload
const nextQueryConfig = applyFuncsToField( const nextQueryConfig = applyFuncsToField(
state[queryId], state[queryID],
fieldFunc, fieldFunc,
groupBy groupBy
) )
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_CHOOSE_TAG': { case 'DE_CHOOSE_TAG': {
const {queryId, tag} = action.payload const {queryID, tag} = action.payload
const nextQueryConfig = chooseTag(state[queryId], tag) const nextQueryConfig = chooseTag(state[queryID], tag)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_GROUP_BY_TAG': { case 'DE_GROUP_BY_TAG': {
const {queryId, tagKey} = action.payload const {queryID, tagKey} = action.payload
const nextQueryConfig = groupByTag(state[queryId], tagKey) const nextQueryConfig = groupByTag(state[queryID], tagKey)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'DE_FILL': { case 'DE_FILL': {
const {queryId, value} = action.payload const {queryID, value} = action.payload
const nextQueryConfig = fill(state[queryId], value) const nextQueryConfig = fill(state[queryID], value)
return { return {
...state, ...state,
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
} }
} }
@ -171,6 +172,13 @@ const queryConfigs = (state = {}, action) => {
return {...state, [queryID]: nextQuery} return {...state, [queryID]: nextQuery}
} }
case 'DE_TIME_SHIFT': {
const {queryID, shift} = action.payload
const nextQuery = timeShift(state[queryID], shift)
return {...state, [queryID]: nextQuery}
}
} }
return state return state
} }

View File

@ -1,63 +1,63 @@
export const chooseNamespace = (queryId, {database, retentionPolicy}) => ({ export const chooseNamespace = (queryID, {database, retentionPolicy}) => ({
type: 'KAPA_CHOOSE_NAMESPACE', type: 'KAPA_CHOOSE_NAMESPACE',
payload: { payload: {
queryId, queryID,
database, database,
retentionPolicy, retentionPolicy,
}, },
}) })
export const chooseMeasurement = (queryId, measurement) => ({ export const chooseMeasurement = (queryID, measurement) => ({
type: 'KAPA_CHOOSE_MEASUREMENT', type: 'KAPA_CHOOSE_MEASUREMENT',
payload: { payload: {
queryId, queryID,
measurement, measurement,
}, },
}) })
export const chooseTag = (queryId, tag) => ({ export const chooseTag = (queryID, tag) => ({
type: 'KAPA_CHOOSE_TAG', type: 'KAPA_CHOOSE_TAG',
payload: { payload: {
queryId, queryID,
tag, tag,
}, },
}) })
export const groupByTag = (queryId, tagKey) => ({ export const groupByTag = (queryID, tagKey) => ({
type: 'KAPA_GROUP_BY_TAG', type: 'KAPA_GROUP_BY_TAG',
payload: { payload: {
queryId, queryID,
tagKey, tagKey,
}, },
}) })
export const toggleTagAcceptance = queryId => ({ export const toggleTagAcceptance = queryID => ({
type: 'KAPA_TOGGLE_TAG_ACCEPTANCE', type: 'KAPA_TOGGLE_TAG_ACCEPTANCE',
payload: { payload: {
queryId, queryID,
}, },
}) })
export const toggleField = (queryId, fieldFunc) => ({ export const toggleField = (queryID, fieldFunc) => ({
type: 'KAPA_TOGGLE_FIELD', type: 'KAPA_TOGGLE_FIELD',
payload: { payload: {
queryId, queryID,
fieldFunc, fieldFunc,
}, },
}) })
export const applyFuncsToField = (queryId, fieldFunc) => ({ export const applyFuncsToField = (queryID, fieldFunc) => ({
type: 'KAPA_APPLY_FUNCS_TO_FIELD', type: 'KAPA_APPLY_FUNCS_TO_FIELD',
payload: { payload: {
queryId, queryID,
fieldFunc, fieldFunc,
}, },
}) })
export const groupByTime = (queryId, time) => ({ export const groupByTime = (queryID, time) => ({
type: 'KAPA_GROUP_BY_TIME', type: 'KAPA_GROUP_BY_TIME',
payload: { payload: {
queryId, queryID,
time, time,
}, },
}) })
@ -69,3 +69,11 @@ export const removeFuncs = (queryID, fields) => ({
fields, fields,
}, },
}) })
export const timeShift = (queryID, shift) => ({
type: 'KAPA_TIME_SHIFT',
payload: {
queryID,
shift,
},
})

View File

@ -66,7 +66,7 @@ export const getRule = (kapacitor, ruleID) => async dispatch => {
} }
} }
export function loadDefaultRule() { export const loadDefaultRule = () => {
return dispatch => { return dispatch => {
const queryID = uuid.v4() const queryID = uuid.v4()
dispatch({ dispatch({
@ -88,15 +88,13 @@ export const fetchRules = kapacitor => async dispatch => {
} }
} }
export function chooseTrigger(ruleID, trigger) { export const chooseTrigger = (ruleID, trigger) => ({
return { type: 'CHOOSE_TRIGGER',
type: 'CHOOSE_TRIGGER', payload: {
payload: { ruleID,
ruleID, trigger,
trigger, },
}, })
}
}
export const addEvery = (ruleID, frequency) => ({ export const addEvery = (ruleID, frequency) => ({
type: 'ADD_EVERY', type: 'ADD_EVERY',
@ -113,36 +111,30 @@ export const removeEvery = ruleID => ({
}, },
}) })
export function updateRuleValues(ruleID, trigger, values) { export const updateRuleValues = (ruleID, trigger, values) => ({
return { type: 'UPDATE_RULE_VALUES',
type: 'UPDATE_RULE_VALUES', payload: {
payload: { ruleID,
ruleID, trigger,
trigger, values,
values, },
}, })
}
}
export function updateMessage(ruleID, message) { export const updateMessage = (ruleID, message) => ({
return { type: 'UPDATE_RULE_MESSAGE',
type: 'UPDATE_RULE_MESSAGE', payload: {
payload: { ruleID,
ruleID, message,
message, },
}, })
}
}
export function updateDetails(ruleID, details) { export const updateDetails = (ruleID, details) => ({
return { type: 'UPDATE_RULE_DETAILS',
type: 'UPDATE_RULE_DETAILS', payload: {
payload: { ruleID,
ruleID, details,
details, },
}, })
}
}
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({ export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
type: 'UPDATE_RULE_ALERT_PROPERTY', type: 'UPDATE_RULE_ALERT_PROPERTY',
@ -153,87 +145,73 @@ export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
}, },
}) })
export function updateAlerts(ruleID, alerts) { export const updateAlerts = (ruleID, alerts) => ({
return { type: 'UPDATE_RULE_ALERTS',
type: 'UPDATE_RULE_ALERTS', payload: {
payload: { ruleID,
ruleID, alerts,
alerts, },
}, })
}
export const updateAlertNodes = (ruleID, alertNodeName, alertNodesText) => ({
type: 'UPDATE_RULE_ALERT_NODES',
payload: {
ruleID,
alertNodeName,
alertNodesText,
},
})
export const updateRuleName = (ruleID, name) => ({
type: 'UPDATE_RULE_NAME',
payload: {
ruleID,
name,
},
})
export const deleteRuleSuccess = ruleID => ({
type: 'DELETE_RULE_SUCCESS',
payload: {
ruleID,
},
})
export const updateRuleStatusSuccess = (ruleID, status) => ({
type: 'UPDATE_RULE_STATUS_SUCCESS',
payload: {
ruleID,
status,
},
})
export const deleteRule = rule => dispatch => {
deleteRuleAPI(rule)
.then(() => {
dispatch(deleteRuleSuccess(rule.id))
dispatch(
publishNotification('success', `${rule.name} deleted successfully`)
)
})
.catch(() => {
dispatch(
publishNotification('error', `${rule.name} could not be deleted`)
)
})
} }
export function updateAlertNodes(ruleID, alertNodeName, alertNodesText) { export const updateRuleStatus = (rule, status) => dispatch => {
return { updateRuleStatusAPI(rule, status)
type: 'UPDATE_RULE_ALERT_NODES', .then(() => {
payload: { dispatch(
ruleID, publishNotification('success', `${rule.name} ${status} successfully`)
alertNodeName, )
alertNodesText, })
}, .catch(() => {
} dispatch(
} publishNotification('error', `${rule.name} could not be ${status}`)
)
export function updateRuleName(ruleID, name) { })
return {
type: 'UPDATE_RULE_NAME',
payload: {
ruleID,
name,
},
}
}
export function deleteRuleSuccess(ruleID) {
return {
type: 'DELETE_RULE_SUCCESS',
payload: {
ruleID,
},
}
}
export function updateRuleStatusSuccess(ruleID, status) {
return {
type: 'UPDATE_RULE_STATUS_SUCCESS',
payload: {
ruleID,
status,
},
}
}
export function deleteRule(rule) {
return dispatch => {
deleteRuleAPI(rule)
.then(() => {
dispatch(deleteRuleSuccess(rule.id))
dispatch(
publishNotification('success', `${rule.name} deleted successfully`)
)
})
.catch(() => {
dispatch(
publishNotification('error', `${rule.name} could not be deleted`)
)
})
}
}
export function updateRuleStatus(rule, status) {
return dispatch => {
updateRuleStatusAPI(rule, status)
.then(() => {
dispatch(
publishNotification('success', `${rule.name} ${status} successfully`)
)
})
.catch(() => {
dispatch(
publishNotification('error', `${rule.name} could not be ${status}`)
)
})
}
} }
export const createTask = ( export const createTask = (

View File

@ -61,13 +61,13 @@ class KapacitorRulePage extends Component {
render() { render() {
const { const {
rules, rules,
queryConfigs,
params, params,
ruleActions,
source, source,
queryConfigActions,
addFlashMessage,
router, router,
ruleActions,
queryConfigs,
addFlashMessage,
queryConfigActions,
} = this.props } = this.props
const {enabledAlerts, kapacitor} = this.state const {enabledAlerts, kapacitor} = this.state
const rule = this.isEditing() const rule = this.isEditing()
@ -80,17 +80,17 @@ class KapacitorRulePage extends Component {
} }
return ( return (
<KapacitorRule <KapacitorRule
source={source}
rule={rule} rule={rule}
query={query} query={query}
queryConfigs={queryConfigs}
queryConfigActions={queryConfigActions}
ruleActions={ruleActions}
addFlashMessage={addFlashMessage}
enabledAlerts={enabledAlerts}
isEditing={this.isEditing()}
router={router} router={router}
source={source}
kapacitor={kapacitor} kapacitor={kapacitor}
ruleActions={ruleActions}
queryConfigs={queryConfigs}
isEditing={this.isEditing()}
enabledAlerts={enabledAlerts}
addFlashMessage={addFlashMessage}
queryConfigActions={queryConfigActions}
/> />
) )
} }

View File

@ -1,13 +1,14 @@
import defaultQueryConfig from 'src/utils/defaultQueryConfig' import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import { import {
applyFuncsToField, timeShift,
chooseMeasurement,
chooseNamespace,
chooseTag, chooseTag,
groupByTag, groupByTag,
groupByTime, groupByTime,
removeFuncs, removeFuncs,
chooseNamespace,
toggleKapaField, toggleKapaField,
applyFuncsToField,
chooseMeasurement,
toggleTagAcceptance, toggleTagAcceptance,
} from 'src/utils/queryTransitions' } from 'src/utils/queryTransitions'
@ -34,9 +35,9 @@ const queryConfigs = (state = {}, action) => {
} }
case 'KAPA_CHOOSE_NAMESPACE': { case 'KAPA_CHOOSE_NAMESPACE': {
const {queryId, database, retentionPolicy} = action.payload const {queryID, database, retentionPolicy} = action.payload
const nextQueryConfig = chooseNamespace( const nextQueryConfig = chooseNamespace(
state[queryId], state[queryID],
{ {
database, database,
retentionPolicy, retentionPolicy,
@ -45,75 +46,75 @@ const queryConfigs = (state = {}, action) => {
) )
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: Object.assign(nextQueryConfig, {rawText: null}), [queryID]: Object.assign(nextQueryConfig, {rawText: null}),
}) })
} }
case 'KAPA_CHOOSE_MEASUREMENT': { case 'KAPA_CHOOSE_MEASUREMENT': {
const {queryId, measurement} = action.payload const {queryID, measurement} = action.payload
const nextQueryConfig = chooseMeasurement( const nextQueryConfig = chooseMeasurement(
state[queryId], state[queryID],
measurement, measurement,
IS_KAPACITOR_RULE IS_KAPACITOR_RULE
) )
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: Object.assign(nextQueryConfig, { [queryID]: Object.assign(nextQueryConfig, {
rawText: state[queryId].rawText, rawText: state[queryID].rawText,
}), }),
}) })
} }
case 'KAPA_CHOOSE_TAG': { case 'KAPA_CHOOSE_TAG': {
const {queryId, tag} = action.payload const {queryID, tag} = action.payload
const nextQueryConfig = chooseTag(state[queryId], tag) const nextQueryConfig = chooseTag(state[queryID], tag)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'KAPA_GROUP_BY_TAG': { case 'KAPA_GROUP_BY_TAG': {
const {queryId, tagKey} = action.payload const {queryID, tagKey} = action.payload
const nextQueryConfig = groupByTag(state[queryId], tagKey) const nextQueryConfig = groupByTag(state[queryID], tagKey)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'KAPA_TOGGLE_TAG_ACCEPTANCE': { case 'KAPA_TOGGLE_TAG_ACCEPTANCE': {
const {queryId} = action.payload const {queryID} = action.payload
const nextQueryConfig = toggleTagAcceptance(state[queryId]) const nextQueryConfig = toggleTagAcceptance(state[queryID])
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
case 'KAPA_TOGGLE_FIELD': { case 'KAPA_TOGGLE_FIELD': {
const {queryId, fieldFunc} = action.payload const {queryID, fieldFunc} = action.payload
const nextQueryConfig = toggleKapaField(state[queryId], fieldFunc) const nextQueryConfig = toggleKapaField(state[queryID], fieldFunc)
return {...state, [queryId]: {...nextQueryConfig, rawText: null}} return {...state, [queryID]: {...nextQueryConfig, rawText: null}}
} }
case 'KAPA_APPLY_FUNCS_TO_FIELD': { case 'KAPA_APPLY_FUNCS_TO_FIELD': {
const {queryId, fieldFunc} = action.payload const {queryID, fieldFunc} = action.payload
const {groupBy} = state[queryId] const {groupBy} = state[queryID]
const nextQueryConfig = applyFuncsToField(state[queryId], fieldFunc, { const nextQueryConfig = applyFuncsToField(state[queryID], fieldFunc, {
...groupBy, ...groupBy,
time: groupBy.time ? groupBy.time : '10s', time: groupBy.time ? groupBy.time : '10s',
}) })
return {...state, [queryId]: nextQueryConfig} return {...state, [queryID]: nextQueryConfig}
} }
case 'KAPA_GROUP_BY_TIME': { case 'KAPA_GROUP_BY_TIME': {
const {queryId, time} = action.payload const {queryID, time} = action.payload
const nextQueryConfig = groupByTime(state[queryId], time) const nextQueryConfig = groupByTime(state[queryID], time)
return Object.assign({}, state, { return Object.assign({}, state, {
[queryId]: nextQueryConfig, [queryID]: nextQueryConfig,
}) })
} }
@ -124,6 +125,13 @@ const queryConfigs = (state = {}, action) => {
// fields with no functions cannot have a group by time // fields with no functions cannot have a group by time
return {...state, [queryID]: nextQuery} return {...state, [queryID]: nextQuery}
} }
case 'KAPA_TIME_SHIFT': {
const {queryID, shift} = action.payload
const nextQuery = timeShift(state[queryID], shift)
return {...state, [queryID]: nextQuery}
}
} }
return state return state
} }

View File

@ -24,8 +24,8 @@ export function showQueries(source, db) {
return proxy({source, query, db}) return proxy({source, query, db})
} }
export function killQuery(source, queryId) { export function killQuery(source, queryID) {
const query = `KILL QUERY ${queryId}` const query = `KILL QUERY ${queryID}`
return proxy({source, query}) return proxy({source, query})
} }

View File

@ -81,20 +81,35 @@ const AutoRefresh = ComposedComponent => {
const templatesWithResolution = templates.map(temp => { const templatesWithResolution = templates.map(temp => {
if (temp.tempVar === ':interval:') { if (temp.tempVar === ':interval:') {
if (resolution) { if (resolution) {
return {...temp, resolution} return {
...temp,
values: temp.values.map(
v => (temp.type === 'resolution' ? {...v, resolution} : v)
),
}
}
return {
...temp,
values: [
...temp.values,
{value: '1000', type: 'resolution', selected: true},
],
} }
return {...temp, resolution: 1000}
} }
return {...temp}
return temp
}) })
const tempVars = removeUnselectedTemplateValues(templatesWithResolution)
return fetchTimeSeriesAsync( return fetchTimeSeriesAsync(
{ {
source: host, source: host,
db: database, db: database,
rp, rp,
query, query,
tempVars: removeUnselectedTemplateValues(templatesWithResolution), tempVars,
resolution, resolution,
}, },
editQueryStatus editQueryStatus

View File

@ -1,9 +1,8 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import _ from 'lodash' import _ from 'lodash'
import QueryOptions from 'shared/components/QueryOptions'
import FieldListItem from 'src/data_explorer/components/FieldListItem' import FieldListItem from 'src/data_explorer/components/FieldListItem'
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
import FillQuery from 'shared/components/FillQuery'
import FancyScrollbar from 'shared/components/FancyScrollbar' import FancyScrollbar from 'shared/components/FancyScrollbar'
import {showFieldKeys} from 'shared/apis/metaQuery' import {showFieldKeys} from 'shared/apis/metaQuery'
@ -107,6 +106,10 @@ class FieldList extends Component {
applyFuncsToField(fieldFunc, groupBy) applyFuncsToField(fieldFunc, groupBy)
} }
handleTimeShift = shift => {
this.props.onTimeShift(shift)
}
_getFields = () => { _getFields = () => {
const {database, measurement, retentionPolicy} = this.props.query const {database, measurement, retentionPolicy} = this.props.query
const {source} = this.context const {source} = this.context
@ -129,12 +132,11 @@ class FieldList extends Component {
render() { render() {
const { const {
query: {database, measurement, fields = [], groupBy, fill}, query: {database, measurement, fields = [], groupBy, fill, shifts},
isKapacitorRule, isKapacitorRule,
} = this.props } = this.props
const hasAggregates = numFunctions(fields) > 0 const hasAggregates = numFunctions(fields) > 0
const hasGroupByTime = groupBy.time
const noDBorMeas = !database || !measurement const noDBorMeas = !database || !measurement
return ( return (
@ -142,16 +144,15 @@ class FieldList extends Component {
<div className="query-builder--heading"> <div className="query-builder--heading">
<span>Fields</span> <span>Fields</span>
{hasAggregates {hasAggregates
? <div className="query-builder--groupby-fill-container"> ? <QueryOptions
<GroupByTimeDropdown fill={fill}
isOpen={!hasGroupByTime} shift={_.first(shifts)}
selected={groupBy.time} groupBy={groupBy}
onChooseGroupByTime={this.handleGroupByTime} onFill={this.handleFill}
/> isKapacitorRule={isKapacitorRule}
{isKapacitorRule onTimeShift={this.handleTimeShift}
? null onGroupByTime={this.handleGroupByTime}
: <FillQuery value={fill} onChooseFill={this.handleFill} />} />
</div>
: null} : null}
</div> </div>
{noDBorMeas {noDBorMeas
@ -192,7 +193,7 @@ class FieldList extends Component {
} }
} }
const {bool, func, shape, string} = PropTypes const {arrayOf, bool, func, shape, string} = PropTypes
FieldList.defaultProps = { FieldList.defaultProps = {
isKapacitorRule: false, isKapacitorRule: false,
@ -212,7 +213,15 @@ FieldList.propTypes = {
database: string, database: string,
retentionPolicy: string, retentionPolicy: string,
measurement: string, measurement: string,
shifts: arrayOf(
shape({
label: string,
unit: string,
quantity: string,
})
),
}).isRequired, }).isRequired,
onTimeShift: func,
onToggleField: func.isRequired, onToggleField: func.isRequired,
onGroupByTime: func.isRequired, onGroupByTime: func.isRequired,
onFill: func, onFill: func,

View File

@ -2,7 +2,7 @@ import React, {Component, PropTypes} from 'react'
import WidgetCell from 'shared/components/WidgetCell' import WidgetCell from 'shared/components/WidgetCell'
import LayoutCell from 'shared/components/LayoutCell' import LayoutCell from 'shared/components/LayoutCell'
import RefreshingGraph from 'shared/components/RefreshingGraph' import RefreshingGraph from 'shared/components/RefreshingGraph'
import {buildQueriesForLayouts} from 'utils/influxql' import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts'
import _ from 'lodash' import _ from 'lodash'

View File

@ -17,12 +17,8 @@ class LineGraph extends Component {
} }
componentWillMount() { componentWillMount() {
const {data, activeQueryIndex, isInDataExplorer} = this.props const {data, isInDataExplorer} = this.props
this._timeSeries = timeSeriesToDygraph( this._timeSeries = timeSeriesToDygraph(data, isInDataExplorer)
data,
activeQueryIndex,
isInDataExplorer
)
} }
componentWillUpdate(nextProps) { componentWillUpdate(nextProps) {
@ -33,7 +29,6 @@ class LineGraph extends Component {
) { ) {
this._timeSeries = timeSeriesToDygraph( this._timeSeries = timeSeriesToDygraph(
nextProps.data, nextProps.data,
nextProps.activeQueryIndex,
nextProps.isInDataExplorer nextProps.isInDataExplorer
) )
} }

View File

@ -0,0 +1,45 @@
import React, {PropTypes} from 'react'
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
import TimeShiftDropdown from 'src/shared/components/TimeShiftDropdown'
import FillQuery from 'shared/components/FillQuery'
const QueryOptions = ({
fill,
shift,
onFill,
groupBy,
onTimeShift,
onGroupByTime,
isKapacitorRule,
}) =>
<div className="query-builder--groupby-fill-container">
<GroupByTimeDropdown
selected={groupBy.time}
onChooseGroupByTime={onGroupByTime}
/>
{isKapacitorRule
? null
: <TimeShiftDropdown
selected={shift && shift.label}
onChooseTimeShift={onTimeShift}
/>}
{isKapacitorRule ? null : <FillQuery value={fill} onChooseFill={onFill} />}
</div>
const {bool, func, shape, string} = PropTypes
QueryOptions.propTypes = {
fill: string,
onFill: func.isRequired,
groupBy: shape({
time: string,
}).isRequired,
shift: shape({
label: string,
}),
onGroupByTime: func.isRequired,
isKapacitorRule: bool.isRequired,
onTimeShift: func.isRequired,
}
export default QueryOptions

View File

@ -13,6 +13,7 @@ const SchemaExplorer = ({
initialGroupByTime, initialGroupByTime,
actions: { actions: {
fill, fill,
timeShift,
chooseTag, chooseTag,
groupByTag, groupByTag,
groupByTime, groupByTime,
@ -44,13 +45,14 @@ const SchemaExplorer = ({
source={source} source={source}
query={query} query={query}
querySource={source} querySource={source}
initialGroupByTime={initialGroupByTime}
onToggleField={actionBinder(id, toggleField)}
onFill={actionBinder(id, fill)} onFill={actionBinder(id, fill)}
onGroupByTime={actionBinder(id, groupByTime)} initialGroupByTime={initialGroupByTime}
applyFuncsToField={actionBinder(id, applyFuncsToField)} onTimeShift={actionBinder(id, timeShift)}
removeFuncs={actionBinder(id, removeFuncs)} removeFuncs={actionBinder(id, removeFuncs)}
onToggleField={actionBinder(id, toggleField)}
onGroupByTime={actionBinder(id, groupByTime)}
addInitialField={actionBinder(id, addInitialField)} addInitialField={actionBinder(id, addInitialField)}
applyFuncsToField={actionBinder(id, applyFuncsToField)}
/> />
</div> </div>

View File

@ -0,0 +1,24 @@
import React, {PropTypes} from 'react'
import Dropdown from 'shared/components/Dropdown'
import {TIME_SHIFTS} from 'shared/constants/timeShift'
const TimeShiftDropdown = ({selected, onChooseTimeShift}) =>
<div className="group-by-time">
<label className="group-by-time--label">Compare:</label>
<Dropdown
className="group-by-time--dropdown"
buttonColor="btn-info"
items={TIME_SHIFTS}
onChoose={onChooseTimeShift}
selected={selected || 'none'}
/>
</div>
const {func, string} = PropTypes
TimeShiftDropdown.propTypes = {
selected: string,
onChooseTimeShift: func.isRequired,
}
export default TimeShiftDropdown

View File

@ -0,0 +1,4 @@
export const ABSOLUTE = 'absolute'
export const INVALID = 'invalid'
export const RELATIVE_LOWER = 'relative lower'
export const RELATIVE_UPPER = 'relative upper'

View File

@ -0,0 +1,10 @@
export const TIME_SHIFTS = [
{label: 'none', text: 'none', quantity: null, unit: null},
{label: '1m', text: '1m', quantity: '1', unit: 'm'},
{label: '1h', text: '1h', quantity: '1', unit: 'h'},
{label: '12h', text: '12h', quantity: '12', unit: 'h'},
{label: '1d', text: '1d', quantity: '1', unit: 'd'},
{label: '7d', text: '7d', quantity: '7', unit: 'd'},
{label: '30d', text: '30d', quantity: '30', unit: 'd'},
{label: '365d', text: '365d', quantity: '365', unit: 'd'},
]

View File

@ -0,0 +1,110 @@
import moment from 'moment'
import {
INFLUXQL,
ABSOLUTE,
INVALID,
RELATIVE_LOWER,
RELATIVE_UPPER,
} from 'shared/constants/timeRange'
const now = /^now/
export const timeRangeType = ({upper, lower, type}) => {
if (!upper && !lower) {
return INVALID
}
if (type && type !== INFLUXQL) {
return INVALID
}
const isUpperValid = moment(upper).isValid()
const isLowerValid = moment(lower).isValid()
// {lower: <Date>, upper: <Date>}
if (isLowerValid && isUpperValid) {
return ABSOLUTE
}
// {lower: now - <Duration>, upper: <empty>}
if (now.test(lower) && !upper) {
return RELATIVE_LOWER
}
// {lower: <Date>, upper: now() - <Duration>}
if (isLowerValid && now.test(upper)) {
return RELATIVE_UPPER
}
return INVALID
}
export const shiftTimeRange = (timeRange, shift) => {
const {upper, lower} = timeRange
const {quantity, unit} = shift
const trType = timeRangeType(timeRange)
const duration = `${quantity}${unit}`
const type = 'shifted'
switch (trType) {
case RELATIVE_UPPER:
case ABSOLUTE: {
return {
lower: `${lower} - ${duration}`,
upper: `${upper} - ${duration}`,
type,
}
}
case RELATIVE_LOWER: {
return {
lower: `${lower} - ${duration}`,
upper: `now() - ${duration}`,
type,
}
}
default: {
return {lower, upper, type: 'unshifted'}
}
}
}
const getMomentUnit = unit => {
switch (unit) {
case 'ms': {
return 'milliseconds' // (1 thousandth of a second)
}
case 's': {
return 'seconds'
}
case 'm': {
return 'minute'
}
case 'h': {
return 'hour'
}
case 'd': {
return 'day'
}
case 'w': {
return 'week'
}
default: {
return unit
}
}
}
export const shiftDate = (date, quantity, unit) => {
if (!date && !quantity && !unit) {
return moment(date)
}
return moment(date).add(quantity, getMomentUnit(unit))
}

View File

@ -138,7 +138,7 @@ $graph-gutter: 16px;
font-size: 20px; font-size: 20px;
font-weight: 400; font-weight: 400;
margin: 0; margin: 0;
text-align: left; text-align: center;
color: $g8-storm; color: $g8-storm;
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@ -1,28 +1,46 @@
import {buildQuery} from 'utils/influxql' import {buildQuery} from 'utils/influxql'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants' import {TYPE_QUERY_CONFIG, TYPE_SHIFTED} from 'src/dashboards/constants'
const buildQueries = (proxy, queryConfigs, timeRange) => { const buildQueries = (proxy, queryConfigs, tR) => {
const statements = queryConfigs.map(query => { const statements = queryConfigs.map(query => {
const text = const {rawText, range, id, shifts, database, measurement, fields} = query
query.rawText || const timeRange = range || tR
buildQuery(TYPE_QUERY_CONFIG, query.range || timeRange, query) const text = rawText || buildQuery(TYPE_QUERY_CONFIG, timeRange, query)
return {text, id: query.id, queryConfig: query} const isParsable = database && measurement && fields.length
})
const queries = statements.filter(s => s.text !== null).map(s => { if (shifts && shifts.length && isParsable) {
let queryProxy = '' const shiftedQueries = shifts
if (s.queryConfig.source) { .filter(s => s.unit)
queryProxy = `${s.queryConfig.source.links.proxy}` .map(s => buildQuery(TYPE_SHIFTED, timeRange, query, s))
return {
text: `${text};${shiftedQueries.join(';')}`,
id,
queryConfig: query,
}
} }
return { return {text, id, queryConfig: query}
host: [queryProxy || proxy],
text: s.text,
id: s.id,
queryConfig: s.queryConfig,
}
}) })
const queries = statements
.filter(s => s.text !== null)
.map(({queryConfig, text, id}) => {
let queryProxy = ''
if (queryConfig.source) {
queryProxy = `${queryConfig.source.links.proxy}`
}
const host = [queryProxy || proxy]
return {
host,
text,
id,
queryConfig,
}
})
return queries return queries
} }

View File

@ -0,0 +1,72 @@
import {buildQuery} from 'utils/influxql'
import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import timeRanges from 'hson!shared/data/timeRanges.hson'
const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
const {defaultGroupBy} = timeRanges.find(range => range.lower === lower) || {
defaultGroupBy: '5m',
}
const {wheres, groupbys} = query
let text = query.text
if (upper) {
text += ` where time > '${lower}' AND time < '${upper}'`
} else {
text += ` where time > ${lower}`
}
if (host) {
text += ` and \"host\" = '${host}'`
}
if (wheres && wheres.length > 0) {
text += ` and ${wheres.join(' and ')}`
}
if (groupbys) {
if (groupbys.find(g => g.includes('time'))) {
text += ` group by ${groupbys.join(',')}`
} else if (groupbys.length > 0) {
text += ` group by time(${defaultGroupBy}),${groupbys.join(',')}`
} else {
text += ` group by time(${defaultGroupBy})`
}
} else {
text += ` group by time(${defaultGroupBy})`
}
return text
}
export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
return cell.queries.map(query => {
let queryText
// Canned dashboards use an different a schema different from queryConfig.
if (query.queryConfig) {
const {
queryConfig: {database, measurement, fields, shifts, rawText, range},
} = query
const tR = range || {
upper: ':upperDashboardTime:',
lower: ':dashboardTime:',
}
queryText =
rawText || buildQuery(TYPE_QUERY_CONFIG, tR, query.queryConfig)
const isParsable = database && measurement && fields.length
if (shifts && shifts.length && isParsable) {
const shiftedQueries = shifts
.filter(s => s.unit)
.map(s => buildQuery(TYPE_SHIFTED, timeRange, query.queryConfig, s))
queryText = `${queryText};${shiftedQueries.join(';')}`
}
} else {
queryText = buildCannedDashboardQuery(query, timeRange, host)
}
return {...query, host: source.links.proxy, text: queryText}
})
}

View File

@ -15,6 +15,7 @@ const defaultQueryConfig = ({id, isKapacitorRule = false}) => {
areTagsAccepted: true, areTagsAccepted: true,
rawText: null, rawText: null,
status: null, status: null,
shifts: [],
} }
return isKapacitorRule ? queryConfig : {...queryConfig, fill: NULL_STRING} return isKapacitorRule ? queryConfig : {...queryConfig, fill: NULL_STRING}

View File

@ -2,8 +2,12 @@ import _ from 'lodash'
import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'shared/constants' import {TEMP_VAR_INTERVAL, AUTO_GROUP_BY} from 'shared/constants'
import {NULL_STRING} from 'shared/constants/queryFillOptions' import {NULL_STRING} from 'shared/constants/queryFillOptions'
import {TYPE_QUERY_CONFIG, TYPE_IFQL} from 'src/dashboards/constants' import {
import timeRanges from 'hson!shared/data/timeRanges.hson' TYPE_QUERY_CONFIG,
TYPE_SHIFTED,
TYPE_IFQL,
} from 'src/dashboards/constants'
import {shiftTimeRange} from 'shared/query/helpers'
/* eslint-disable quotes */ /* eslint-disable quotes */
export const quoteIfTimestamp = ({lower, upper}) => { export const quoteIfTimestamp = ({lower, upper}) => {
@ -19,11 +23,11 @@ export const quoteIfTimestamp = ({lower, upper}) => {
} }
/* eslint-enable quotes */ /* eslint-enable quotes */
export default function buildInfluxQLQuery(timeRange, config) { export default function buildInfluxQLQuery(timeRange, config, shift) {
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
const {upper, lower} = quoteIfTimestamp(timeRange) const {upper, lower} = quoteIfTimestamp(timeRange)
const select = _buildSelect(config) const select = _buildSelect(config, shift)
if (select === null) { if (select === null) {
return null return null
} }
@ -35,26 +39,35 @@ export default function buildInfluxQLQuery(timeRange, config) {
return `${select}${condition}${dimensions}${fillClause}` return `${select}${condition}${dimensions}${fillClause}`
} }
function _buildSelect({fields, database, retentionPolicy, measurement}) { function _buildSelect({fields, database, retentionPolicy, measurement}, shift) {
if (!database || !measurement || !fields || !fields.length) { if (!database || !measurement || !fields || !fields.length) {
return null return null
} }
const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : '' const rpSegment = retentionPolicy ? `"${retentionPolicy}"` : ''
const fieldsClause = _buildFields(fields) const fieldsClause = _buildFields(fields, shift)
const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"` const fullyQualifiedMeasurement = `"${database}".${rpSegment}."${measurement}"`
const statement = `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}` const statement = `SELECT ${fieldsClause} FROM ${fullyQualifiedMeasurement}`
return statement return statement
} }
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig // type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
export const buildQuery = (type, timeRange, config) => { export const buildQuery = (type, timeRange, config, shift) => {
switch (type) { switch (type) {
case `${TYPE_QUERY_CONFIG}`: { case TYPE_QUERY_CONFIG: {
return buildInfluxQLQuery(timeRange, config) return buildInfluxQLQuery(timeRange, config)
} }
case `${TYPE_IFQL}`: { case TYPE_SHIFTED: {
const {quantity, unit} = shift
return buildInfluxQLQuery(
shiftTimeRange(timeRange, shift),
config,
`_shifted__${quantity}__${unit}`
)
}
case TYPE_IFQL: {
// build query usining IFQL here // build query usining IFQL here
} }
} }
@ -66,7 +79,7 @@ export function buildSelectStatement(config) {
return _buildSelect(config) return _buildSelect(config)
} }
function _buildFields(fieldFuncs) { function _buildFields(fieldFuncs, shift = '') {
if (!fieldFuncs) { if (!fieldFuncs) {
return '' return ''
} }
@ -77,9 +90,21 @@ function _buildFields(fieldFuncs) {
case 'field': { case 'field': {
return f.value === '*' ? '*' : `"${f.value}"` return f.value === '*' ? '*' : `"${f.value}"`
} }
case 'wildcard': {
return '*'
}
case 'regex': {
return `/${f.value}/`
}
case 'number': {
return `${f.value}`
}
case 'integer': {
return `${f.value}`
}
case 'func': { case 'func': {
const args = _buildFields(f.args) const args = _buildFields(f.args)
const alias = f.alias ? ` AS "${f.alias}"` : '' const alias = f.alias ? ` AS "${f.alias}${shift}"` : ''
return `${f.value}(${args})${alias}` return `${f.value}(${args})${alias}`
} }
} }
@ -155,61 +180,5 @@ function _buildFill(fill) {
return ` FILL(${fill})` return ` FILL(${fill})`
} }
const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
const {defaultGroupBy} = timeRanges.find(range => range.lower === lower) || {
defaultGroupBy: '5m',
}
const {wheres, groupbys} = query
let text = query.text
if (upper) {
text += ` where time > '${lower}' AND time < '${upper}'`
} else {
text += ` where time > ${lower}`
}
if (host) {
text += ` and \"host\" = '${host}'`
}
if (wheres && wheres.length > 0) {
text += ` and ${wheres.join(' and ')}`
}
if (groupbys) {
if (groupbys.find(g => g.includes('time'))) {
text += ` group by ${groupbys.join(',')}`
} else if (groupbys.length > 0) {
text += ` group by time(${defaultGroupBy}),${groupbys.join(',')}`
} else {
text += ` group by time(${defaultGroupBy})`
}
} else {
text += ` group by time(${defaultGroupBy})`
}
return text
}
export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
return cell.queries.map(query => {
let queryText
// Canned dashboards use an different a schema different from queryConfig.
if (query.queryConfig) {
const {queryConfig: {rawText, range}} = query
const tR = range || {
upper: ':upperDashboardTime:',
lower: ':dashboardTime:',
}
queryText = rawText || buildInfluxQLQuery(tR, query.queryConfig)
} else {
queryText = buildCannedDashboardQuery(query, timeRange, host)
}
return {...query, host: source.links.proxy, text: queryText}
})
}
export const buildRawText = (q, timeRange) => export const buildRawText = (q, timeRange) =>
q.rawText || buildInfluxQLQuery(timeRange, q) || '' q.rawText || buildInfluxQLQuery(timeRange, q) || ''

View File

@ -108,7 +108,7 @@ export const toggleField = (query, {value}) => {
} }
} }
export function groupByTime(query, time) { export const groupByTime = (query, time) => {
return Object.assign({}, query, { return Object.assign({}, query, {
groupBy: Object.assign({}, query.groupBy, { groupBy: Object.assign({}, query.groupBy, {
time, time,
@ -118,7 +118,7 @@ export function groupByTime(query, time) {
export const fill = (query, value) => ({...query, fill: value}) export const fill = (query, value) => ({...query, fill: value})
export function toggleTagAcceptance(query) { export const toggleTagAcceptance = query => {
return Object.assign({}, query, { return Object.assign({}, query, {
areTagsAccepted: !query.areTagsAccepted, areTagsAccepted: !query.areTagsAccepted,
}) })
@ -185,13 +185,13 @@ export const applyFuncsToField = (query, {field, funcs = []}, groupBy) => {
} }
} }
export function updateRawQuery(query, rawText) { export const updateRawQuery = (query, rawText) => {
return Object.assign({}, query, { return Object.assign({}, query, {
rawText, rawText,
}) })
} }
export function groupByTag(query, tagKey) { export const groupByTag = (query, tagKey) => {
const oldTags = query.groupBy.tags const oldTags = query.groupBy.tags
let newTags let newTags
@ -209,7 +209,7 @@ export function groupByTag(query, tagKey) {
}) })
} }
export function chooseTag(query, tag) { export const chooseTag = (query, tag) => {
const tagValues = query.tags[tag.key] const tagValues = query.tags[tag.key]
const shouldRemoveTag = const shouldRemoveTag =
tagValues && tagValues.length === 1 && tagValues[0] === tag.value tagValues && tagValues.length === 1 && tagValues[0] === tag.value
@ -219,6 +219,14 @@ export function chooseTag(query, tag) {
return Object.assign({}, query, {tags: newTags}) return Object.assign({}, query, {tags: newTags})
} }
const updateTagValues = newTagValues => {
return Object.assign({}, query, {
tags: Object.assign({}, query.tags, {
[tag.key]: newTagValues,
}),
})
}
const oldTagValues = query.tags[tag.key] const oldTagValues = query.tags[tag.key]
if (!oldTagValues) { if (!oldTagValues) {
return updateTagValues([tag.value]) return updateTagValues([tag.value])
@ -233,12 +241,6 @@ export function chooseTag(query, tag) {
} }
return updateTagValues(query.tags[tag.key].concat(tag.value)) return updateTagValues(query.tags[tag.key].concat(tag.value))
function updateTagValues(newTagValues) {
return Object.assign({}, query, {
tags: Object.assign({}, query.tags, {
[tag.key]: newTagValues,
}),
})
}
} }
export const timeShift = (query, shift) => ({...query, shifts: [shift]})

View File

@ -1,4 +1,5 @@
import _ from 'lodash' import _ from 'lodash'
import {shiftDate} from 'shared/query/helpers'
import {map, reduce, forEach, concat, clone} from 'fast.js' import {map, reduce, forEach, concat, clone} from 'fast.js'
/** /**
@ -15,12 +16,7 @@ const cells = {
responseIndex: new Array(DEFAULT_SIZE), responseIndex: new Array(DEFAULT_SIZE),
} }
// activeQueryIndex is an optional argument that indicated which query's series we want highlighted. export default function timeSeriesToDygraph(raw = [], isInDataExplorer) {
export default function timeSeriesToDygraph(
raw = [],
activeQueryIndex,
isInDataExplorer
) {
// collect results from each influx response // collect results from each influx response
const results = reduce( const results = reduce(
raw, raw,
@ -115,11 +111,16 @@ export default function timeSeriesToDygraph(
const timeSeries = [] const timeSeries = []
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
const time = cells.time[i] let time = cells.time[i]
const value = cells.value[i] const value = cells.value[i]
const label = cells.label[i] const label = cells.label[i]
const seriesIndex = cells.seriesIndex[i] const seriesIndex = cells.seriesIndex[i]
if (label.includes('_shifted__')) {
const [, quantity, duration] = label.split('__')
time = +shiftDate(time, quantity, duration).format('x')
}
let existingRowIndex = tsMemo[time] let existingRowIndex = tsMemo[time]
if (existingRowIndex === undefined) { if (existingRowIndex === undefined) {