diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index 3e7aa0541a8..66fff11a924 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/tsdb" @@ -102,6 +104,15 @@ func init() { }, }) + registerScenario(&Scenario{ + Id: "predictable_pulse", + Name: "Predictable Pulse", + Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { + return getPredictablePulse(query, context) + }, + Description: PredictablePulseDesc, + }) + registerScenario(&Scenario{ Id: "random_walk_table", Name: "Random Walk Table", @@ -342,6 +353,101 @@ func init() { }) } +// PredictablePulseDesc is the description for the Predictable Pulse scenerio. +const PredictablePulseDesc = `Predictable Pulse returns a pulse wave where there is a datapoint every timeStepSeconds. +The wave cycles at timeStepSeconds*(onCount+offCount). +The cycle of the wave is based off of absolute time (from the epoch) which makes it predictable. +Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means times will all end in :00 seconds).` + +func getPredictablePulse(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { + queryRes := tsdb.NewQueryResult() + + // Process Input + var timeStep int64 + var onCount int64 + var offCount int64 + var onValue null.Float + var offValue null.Float + + options := query.Model.Get("pulseWave") + + var err error + if timeStep, err = options.Get("timeStep").Int64(); err != nil { + queryRes.Error = fmt.Errorf("failed to parse timeStep value '%v' into integer: %v", options.Get("timeStep"), err) + return queryRes + } + if onCount, err = options.Get("onCount").Int64(); err != nil { + queryRes.Error = fmt.Errorf("failed to parse onCount value '%v' into integer: %v", options.Get("onCount"), err) + return queryRes + } + if offCount, err = options.Get("offCount").Int64(); err != nil { + queryRes.Error = fmt.Errorf("failed to parse offCount value '%v' into integer: %v", options.Get("offCount"), err) + return queryRes + } + + fromStringOrNumber := func(val *simplejson.Json) (null.Float, error) { + switch v := val.Interface().(type) { + case json.Number: + fV, err := v.Float64() + if err != nil { + return null.Float{}, err + } + return null.FloatFrom(fV), nil + case string: + if v == "null" { + return null.FloatFromPtr(nil), nil + } + fV, err := strconv.ParseFloat(v, 64) + if err != nil { + return null.Float{}, err + } + return null.FloatFrom(fV), nil + default: + return null.Float{}, fmt.Errorf("failed to extract value") + } + } + onValue, err = fromStringOrNumber(options.Get("onValue")) + if err != nil { + queryRes.Error = fmt.Errorf("failed to parse onValue value '%v' into float: %v", options.Get("onValue"), err) + return queryRes + } + offValue, err = fromStringOrNumber(options.Get("offValue")) + if err != nil { + queryRes.Error = fmt.Errorf("failed to parse offValue value '%v' into float: %v", options.Get("offValue"), err) + return queryRes + } + + from := context.TimeRange.GetFromAsMsEpoch() + to := context.TimeRange.GetToAsMsEpoch() + + series := newSeriesForQuery(query) + points := make(tsdb.TimeSeriesPoints, 0) + + timeStep = timeStep * 1000 // Seconds to Milliseconds + timeCursor := from - (from % timeStep) // Truncate Start + wavePeriod := timeStep * (onCount + offCount) + maxPoints := 10000 // Don't return too many points + + onFor := func(mod int64) null.Float { // How many items in the cycle should get the on value + var i int64 + for i = 0; i < onCount; i++ { + if mod == i*timeStep { + return onValue + } + } + return offValue + } + for i := 0; i < maxPoints && timeCursor < to; i++ { + point := tsdb.NewTimePoint(onFor(timeCursor%wavePeriod), float64(timeCursor)) + points = append(points, point) + timeCursor += timeStep + } + + series.Points = points + queryRes.Series = append(queryRes.Series, series) + return queryRes +} + func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult { timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch() to := tsdbQuery.TimeRange.GetToAsMsEpoch() diff --git a/public/app/plugins/datasource/testdata/partials/query.editor.html b/public/app/plugins/datasource/testdata/partials/query.editor.html index 4a2f59e749f..03d37058371 100644 --- a/public/app/plugins/datasource/testdata/partials/query.editor.html +++ b/public/app/plugins/datasource/testdata/partials/query.editor.html @@ -116,4 +116,68 @@ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/public/app/plugins/datasource/testdata/query_ctrl.ts b/public/app/plugins/datasource/testdata/query_ctrl.ts index 0df7d4f13df..c3574652a3d 100644 --- a/public/app/plugins/datasource/testdata/query_ctrl.ts +++ b/public/app/plugins/datasource/testdata/query_ctrl.ts @@ -5,6 +5,14 @@ import { defaultQuery } from './StreamHandler'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { dateTime } from '@grafana/data'; +export const defaultPulse: any = { + timeStep: 60, + onCount: 3, + onValue: 2, + offCount: 3, + offValue: 1, +}; + export class TestDataQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; @@ -75,6 +83,12 @@ export class TestDataQueryCtrl extends QueryCtrl { delete this.target.stream; } + if (this.target.scenarioId === 'predictable_pulse') { + this.target.pulseWave = _.defaults(this.target.pulseWave || {}, defaultPulse); + } else { + delete this.target.pulseWave; + } + this.refresh(); }