diff --git a/CHANGELOG.md b/CHANGELOG.md index 6efd34c02b..302aefc22d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,14 @@ ### Bug Fixes 1. [#1364](https://github.com/influxdata/chronograf/pull/1364): Fix link to home when using the --basepath option + 1. [#1370](https://github.com/influxdata/chronograf/pull/1370): Remove notification to login outside of session timeout + 1. [#1376](https://github.com/influxdata/chronograf/pull/1376): Fix queries built in query builder with math functions in fields + ### Features ### UI Improvements + 1. [#1378](https://github.com/influxdata/chronograf/pull/1378): Save query time range for dashboards 1. [#1365](https://github.com/influxdata/chronograf/pull/1365): Show red indicator on Hosts Page for an offline host + 1. [#1373](https://github.com/influxdata/chronograf/pull/1373): Re-address dashboard cell stacking contexts ## v1.2.0-beta10 [2017-04-28] diff --git a/chronograf.go b/chronograf.go index 245b00a87e..f1f2a6ca76 100644 --- a/chronograf.go +++ b/chronograf.go @@ -289,6 +289,12 @@ type GroupBy struct { Tags []string `json:"tags"` } +// DurationRange represents the lower and upper durations of the query config +type DurationRange struct { + Upper string `json:"upper"` + Lower string `json:"lower"` +} + // QueryConfig represents UI query from the data explorer type QueryConfig struct { ID string `json:"id,omitempty"` @@ -300,6 +306,7 @@ type QueryConfig struct { GroupBy GroupBy `json:"groupBy"` AreTagsAccepted bool `json:"areTagsAccepted"` RawText *string `json:"rawText"` + Range *DurationRange `json:"range"` } // KapacitorNode adds arguments and properties to an alert diff --git a/influx/query.go b/influx/query.go index af35779404..9592103e85 100644 --- a/influx/query.go +++ b/influx/query.go @@ -2,6 +2,7 @@ package influx import ( "strings" + "time" "github.com/influxdata/chronograf" "github.com/influxdata/influxdb/influxql" @@ -102,6 +103,8 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) { fields := map[string][]string{} for _, fld := range stmt.Fields { switch f := fld.Expr.(type) { + default: + return raw, nil case *influxql.Call: // only support certain query config functions if _, ok := supportedFuncs[f.Name]; !ok { @@ -172,6 +175,14 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) { } } + // If the condition has a time range we report back its duration + if dur, ok := hasTimeRange(stmt.Condition); ok { + qc.Range = &chronograf.DurationRange{ + Lower: shortDur(dur), + Upper: "now", + } + } + return qc, nil } @@ -200,31 +211,36 @@ func isNow(exp influxql.Expr) bool { return false } -func isDuration(exp influxql.Expr) bool { +func isDuration(exp influxql.Expr) (time.Duration, bool) { switch e := exp.(type) { case *influxql.ParenExpr: return isDuration(e.Expr) - case *influxql.DurationLiteral, *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral: - return true + case *influxql.DurationLiteral: + return e.Val, true + case *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral: + return 0, false } - return false + return 0, false } -func isPreviousTime(exp influxql.Expr) bool { +func isPreviousTime(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return isPreviousTime(p.Expr) } else if bin, ok := exp.(*influxql.BinaryExpr); ok { now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now op := bin.Op == influxql.SUB - dur := isDuration(bin.LHS) || isDuration(bin.RHS) // either side can be a isDuration - return now && op && dur + dur, hasDur := isDuration(bin.LHS) + if !hasDur { + dur, hasDur = isDuration(bin.RHS) + } + return dur, now && op && hasDur } else if isNow(exp) { // just comparing to now - return true + return 0, true } - return false + return 0, false } -func isTimeRange(exp influxql.Expr) bool { +func isTimeRange(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return isTimeRange(p.Expr) } else if bin, ok := exp.(*influxql.BinaryExpr); ok { @@ -234,21 +250,28 @@ func isTimeRange(exp influxql.Expr) bool { case influxql.LT, influxql.LTE, influxql.GT, influxql.GTE: op = true } - prev := isPreviousTime(bin.LHS) || isPreviousTime(bin.RHS) - return tm && op && prev + dur, prev := isPreviousTime(bin.LHS) + if !prev { + dur, prev = isPreviousTime(bin.RHS) + } + return dur, tm && op && prev } - return false + return 0, false } -func hasTimeRange(exp influxql.Expr) bool { +func hasTimeRange(exp influxql.Expr) (time.Duration, bool) { if p, ok := exp.(*influxql.ParenExpr); ok { return hasTimeRange(p.Expr) - } else if isTimeRange(exp) { - return true + } else if dur, ok := isTimeRange(exp); ok { + return dur, true } else if bin, ok := exp.(*influxql.BinaryExpr); ok { - return isTimeRange(bin.LHS) || isTimeRange(bin.RHS) + dur, ok := isTimeRange(bin.LHS) + if !ok { + dur, ok = isTimeRange(bin.RHS) + } + return dur, ok } - return false + return 0, false } func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { @@ -256,7 +279,7 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return isTagLogic(p.Expr) } - if isTimeRange(exp) { + if _, ok := isTimeRange(exp); ok { return nil, true } else if tf, ok := isTagFilter(exp); ok { return []tagFilter{tf}, true @@ -278,7 +301,10 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return nil, false } - tm := isTimeRange(bin.LHS) || isTimeRange(bin.RHS) + _, tm := isTimeRange(bin.LHS) + if !tm { + _, tm = isTimeRange(bin.RHS) + } tf := lhsOK || rhsOK if tm && tf { if lhsOK { @@ -305,35 +331,6 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) { return nil, false } -func hasTagFilter(exp influxql.Expr) bool { - if _, ok := isTagFilter(exp); ok { - return true - } else if p, ok := exp.(*influxql.ParenExpr); ok { - return hasTagFilter(p.Expr) - } else if bin, ok := exp.(*influxql.BinaryExpr); ok { - or := bin.Op == influxql.OR - and := bin.Op == influxql.AND - op := or || and - return op && (hasTagFilter(bin.LHS) || hasTagFilter(bin.RHS)) - } - return false -} - -func singleTagFilter(exp influxql.Expr) (tagFilter, bool) { - if p, ok := exp.(*influxql.ParenExpr); ok { - return singleTagFilter(p.Expr) - } else if tf, ok := isTagFilter(exp); ok { - return tf, true - } else if bin, ok := exp.(*influxql.BinaryExpr); ok && bin.Op == influxql.OR { - lhs, lhsOK := singleTagFilter(bin.LHS) - rhs, rhsOK := singleTagFilter(bin.RHS) - if lhsOK && rhsOK && lhs.Op == rhs.Op && lhs.Tag == rhs.Tag { - return lhs, true - } - } - return tagFilter{}, false -} - func isVarRef(exp influxql.Expr) bool { if p, ok := exp.(*influxql.ParenExpr); ok { return isVarRef(p.Expr) @@ -409,3 +406,15 @@ var supportedFuncs = map[string]bool{ "spread": true, "stddev": true, } + +// shortDur converts duration into the queryConfig duration format +func shortDur(d time.Duration) string { + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = s[:len(s)-2] + } + if strings.HasSuffix(s, "h0m") { + s = s[:len(s)-2] + } + return s +} diff --git a/influx/query_test.go b/influx/query_test.go index 50c95354a5..a890ce9bca 100644 --- a/influx/query_test.go +++ b/influx/query_test.go @@ -27,6 +27,214 @@ func TestConvert(t *testing.T) { }, }, }, + { + name: "Test math", + influxQL: `SELECT count("event_id")/3 as "event_count_id" from discource.autogen.discourse_events where time > now() - 7d group by time(1d), "event_type"`, + RawText: `SELECT count("event_id")/3 as "event_count_id" from discource.autogen.discourse_events where time > now() - 7d group by time(1d), "event_type"`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test invalid range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test range with no duration", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now()`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "0s", + Upper: "now", + }, + }, + }, + { + name: "Test range with no tags", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Tags: map[string][]string{}, + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test range with no tags nor duration", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where time`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where time`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test with no time range", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test with no where clauses", + influxQL: `SELECT usage_user from telegraf.autogen.cpu`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + }, + }, + { + name: "Test tags accepted", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" = 'myhost' and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{"host": []string{"myhost"}}, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: true, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, + { + name: "Test mixed tag logic", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`, + RawText: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`, + want: chronograf.QueryConfig{ + Fields: []chronograf.Field{}, + Tags: map[string][]string{}, + GroupBy: chronograf.GroupBy{ + Tags: []string{}, + }, + }, + }, + { + name: "Test tags accepted", + influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' OR "host" = 'yourhost') and ("these" = 'those') and time > now() - 15m`, + want: chronograf.QueryConfig{ + Database: "telegraf", + Measurement: "cpu", + RetentionPolicy: "autogen", + Fields: []chronograf.Field{ + chronograf.Field{ + Field: "usage_user", + Funcs: []string{}, + }, + }, + Tags: map[string][]string{ + "host": []string{"myhost", "yourhost"}, + "these": []string{"those"}, + }, + GroupBy: chronograf.GroupBy{ + Time: "", + Tags: []string{}, + }, + AreTagsAccepted: true, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -37,6 +245,11 @@ func TestConvert(t *testing.T) { } if tt.RawText != "" { tt.want.RawText = &tt.RawText + if got.RawText == nil { + t.Errorf("Convert() = nil, want %s", tt.RawText) + } else if *got.RawText != tt.RawText { + t.Errorf("Convert() = %s, want %s", *got.RawText, tt.RawText) + } } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Convert() = %#v, want %#v", got, tt.want) diff --git a/server/dashboards_test.go b/server/dashboards_test.go index 73cd59c32f..d090fd73fc 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -283,6 +283,10 @@ func Test_newDashboardResponse(t *testing.T) { }, Tags: make(map[string][]string, 0), AreTagsAccepted: false, + Range: &chronograf.DurationRange{ + Lower: "15m", + Upper: "now", + }, }, }, }, @@ -299,7 +303,7 @@ func Test_newDashboardResponse(t *testing.T) { } for _, tt := range tests { if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) { - t.Errorf("%q. newDashboardResponse() = \n%+v\n\n, want\n\n%+v", tt.name, got, tt.want) + t.Errorf("%q. newDashboardResponse() = \n%#v\n\n, want\n\n%#v", tt.name, got, tt.want) } } } diff --git a/server/swagger.json b/server/swagger.json index 3a358efdf8..1344bc398c 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -2536,7 +2536,11 @@ "time": "10m", "tags": [] }, - "areTagsAccepted": true + "areTagsAccepted": true, + "range": { + "lower": "15m", + "upper": "now" + } }, "properties": { "id": { @@ -2598,6 +2602,21 @@ "funcs" ] } + }, + "range": { + "type": "object", + "properties": { + "lower": { + "type": "string" + }, + "upper": { + "type": "string" + } + }, + "required": [ + "lower", + "upper" + ] } }, "required": [ diff --git a/ui/src/shared/middleware/errors.js b/ui/src/shared/middleware/errors.js index 5cfd3aa9f4..ec3a154897 100644 --- a/ui/src/shared/middleware/errors.js +++ b/ui/src/shared/middleware/errors.js @@ -33,8 +33,6 @@ const errorsMiddleware = store => next => action => { setTimeout(() => { allowNotifications = true }, notificationsBlackoutDuration) - } else { - store.dispatch(notify('error', 'Please login to use Chronograf.')) } } else if (altText) { store.dispatch(notify('error', altText)) diff --git a/ui/src/style/pages/dashboards.scss b/ui/src/style/pages/dashboards.scss index 71dd3f0543..088ef542a2 100644 --- a/ui/src/style/pages/dashboards.scss +++ b/ui/src/style/pages/dashboards.scss @@ -86,7 +86,6 @@ $dash-graph-options-arrow: 8px; top: $dash-graph-heading; left: 0; padding: 0; - z-index: 0; & > div:not(.graph-empty) { position: absolute; @@ -164,7 +163,7 @@ $dash-graph-options-arrow: 8px; .dash-graph--options { width: $dash-graph-heading; position: absolute; - z-index: 1; + z-index: 11; right: 0px; top: 0px;