Merge branch 'master' into condense-de-lists

pull/10616/head
Alex P 2017-05-03 10:09:12 -07:00
commit 232cf73707
8 changed files with 309 additions and 55 deletions

View File

@ -2,9 +2,14 @@
### Bug Fixes ### Bug Fixes
1. [#1364](https://github.com/influxdata/chronograf/pull/1364): Fix link to home when using the --basepath option 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 ### Features
### UI Improvements ### 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. [#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] ## v1.2.0-beta10 [2017-04-28]

View File

@ -289,6 +289,12 @@ type GroupBy struct {
Tags []string `json:"tags"` 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 // QueryConfig represents UI query from the data explorer
type QueryConfig struct { type QueryConfig struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
@ -300,6 +306,7 @@ type QueryConfig struct {
GroupBy GroupBy `json:"groupBy"` GroupBy GroupBy `json:"groupBy"`
AreTagsAccepted bool `json:"areTagsAccepted"` AreTagsAccepted bool `json:"areTagsAccepted"`
RawText *string `json:"rawText"` RawText *string `json:"rawText"`
Range *DurationRange `json:"range"`
} }
// KapacitorNode adds arguments and properties to an alert // KapacitorNode adds arguments and properties to an alert

View File

@ -2,6 +2,7 @@ package influx
import ( import (
"strings" "strings"
"time"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/influxdb/influxql" "github.com/influxdata/influxdb/influxql"
@ -102,6 +103,8 @@ func Convert(influxQL string) (chronograf.QueryConfig, error) {
fields := map[string][]string{} fields := map[string][]string{}
for _, fld := range stmt.Fields { for _, fld := range stmt.Fields {
switch f := fld.Expr.(type) { switch f := fld.Expr.(type) {
default:
return raw, nil
case *influxql.Call: case *influxql.Call:
// only support certain query config functions // only support certain query config functions
if _, ok := supportedFuncs[f.Name]; !ok { 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 return qc, nil
} }
@ -200,31 +211,36 @@ func isNow(exp influxql.Expr) bool {
return false return false
} }
func isDuration(exp influxql.Expr) bool { func isDuration(exp influxql.Expr) (time.Duration, bool) {
switch e := exp.(type) { switch e := exp.(type) {
case *influxql.ParenExpr: case *influxql.ParenExpr:
return isDuration(e.Expr) return isDuration(e.Expr)
case *influxql.DurationLiteral, *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral: case *influxql.DurationLiteral:
return true 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 { if p, ok := exp.(*influxql.ParenExpr); ok {
return isPreviousTime(p.Expr) return isPreviousTime(p.Expr)
} else if bin, ok := exp.(*influxql.BinaryExpr); ok { } else if bin, ok := exp.(*influxql.BinaryExpr); ok {
now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now
op := bin.Op == influxql.SUB op := bin.Op == influxql.SUB
dur := isDuration(bin.LHS) || isDuration(bin.RHS) // either side can be a isDuration dur, hasDur := isDuration(bin.LHS)
return now && op && dur if !hasDur {
dur, hasDur = isDuration(bin.RHS)
}
return dur, now && op && hasDur
} else if isNow(exp) { // just comparing to now } 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 { if p, ok := exp.(*influxql.ParenExpr); ok {
return isTimeRange(p.Expr) return isTimeRange(p.Expr)
} else if bin, ok := exp.(*influxql.BinaryExpr); ok { } 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: case influxql.LT, influxql.LTE, influxql.GT, influxql.GTE:
op = true op = true
} }
prev := isPreviousTime(bin.LHS) || isPreviousTime(bin.RHS) dur, prev := isPreviousTime(bin.LHS)
return tm && op && prev 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 { if p, ok := exp.(*influxql.ParenExpr); ok {
return hasTimeRange(p.Expr) return hasTimeRange(p.Expr)
} else if isTimeRange(exp) { } else if dur, ok := isTimeRange(exp); ok {
return true return dur, true
} else if bin, ok := exp.(*influxql.BinaryExpr); ok { } 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) { func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
@ -256,7 +279,7 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
return isTagLogic(p.Expr) return isTagLogic(p.Expr)
} }
if isTimeRange(exp) { if _, ok := isTimeRange(exp); ok {
return nil, true return nil, true
} else if tf, ok := isTagFilter(exp); ok { } else if tf, ok := isTagFilter(exp); ok {
return []tagFilter{tf}, true return []tagFilter{tf}, true
@ -278,7 +301,10 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
return nil, false return nil, false
} }
tm := isTimeRange(bin.LHS) || isTimeRange(bin.RHS) _, tm := isTimeRange(bin.LHS)
if !tm {
_, tm = isTimeRange(bin.RHS)
}
tf := lhsOK || rhsOK tf := lhsOK || rhsOK
if tm && tf { if tm && tf {
if lhsOK { if lhsOK {
@ -305,35 +331,6 @@ func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
return nil, false 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 { func isVarRef(exp influxql.Expr) bool {
if p, ok := exp.(*influxql.ParenExpr); ok { if p, ok := exp.(*influxql.ParenExpr); ok {
return isVarRef(p.Expr) return isVarRef(p.Expr)
@ -409,3 +406,15 @@ var supportedFuncs = map[string]bool{
"spread": true, "spread": true,
"stddev": 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
}

View File

@ -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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -37,6 +245,11 @@ func TestConvert(t *testing.T) {
} }
if tt.RawText != "" { if tt.RawText != "" {
tt.want.RawText = &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) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Convert() = %#v, want %#v", got, tt.want) t.Errorf("Convert() = %#v, want %#v", got, tt.want)

View File

@ -283,6 +283,10 @@ func Test_newDashboardResponse(t *testing.T) {
}, },
Tags: make(map[string][]string, 0), Tags: make(map[string][]string, 0),
AreTagsAccepted: false, AreTagsAccepted: false,
Range: &chronograf.DurationRange{
Lower: "15m",
Upper: "now",
},
}, },
}, },
}, },
@ -299,7 +303,7 @@ func Test_newDashboardResponse(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) { 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)
} }
} }
} }

View File

@ -2536,7 +2536,11 @@
"time": "10m", "time": "10m",
"tags": [] "tags": []
}, },
"areTagsAccepted": true "areTagsAccepted": true,
"range": {
"lower": "15m",
"upper": "now"
}
}, },
"properties": { "properties": {
"id": { "id": {
@ -2598,6 +2602,21 @@
"funcs" "funcs"
] ]
} }
},
"range": {
"type": "object",
"properties": {
"lower": {
"type": "string"
},
"upper": {
"type": "string"
}
},
"required": [
"lower",
"upper"
]
} }
}, },
"required": [ "required": [

View File

@ -33,8 +33,6 @@ const errorsMiddleware = store => next => action => {
setTimeout(() => { setTimeout(() => {
allowNotifications = true allowNotifications = true
}, notificationsBlackoutDuration) }, notificationsBlackoutDuration)
} else {
store.dispatch(notify('error', 'Please login to use Chronograf.'))
} }
} else if (altText) { } else if (altText) {
store.dispatch(notify('error', altText)) store.dispatch(notify('error', altText))

View File

@ -86,7 +86,6 @@ $dash-graph-options-arrow: 8px;
top: $dash-graph-heading; top: $dash-graph-heading;
left: 0; left: 0;
padding: 0; padding: 0;
z-index: 0;
& > div:not(.graph-empty) { & > div:not(.graph-empty) {
position: absolute; position: absolute;
@ -164,7 +163,7 @@ $dash-graph-options-arrow: 8px;
.dash-graph--options { .dash-graph--options {
width: $dash-graph-heading; width: $dash-graph-heading;
position: absolute; position: absolute;
z-index: 1; z-index: 11;
right: 0px; right: 0px;
top: 0px; top: 0px;