Procházet zdrojové kódy

datasource: testdata - add predicatable csv wave scenario (#18183)

Kyle Brandt před 6 roky
rodič
revize
7cac393ddc

+ 14 - 0
pkg/components/null/float.go

@@ -42,6 +42,20 @@ func FloatFromPtr(f *float64) Float {
 	return NewFloat(*f, true)
 	return NewFloat(*f, true)
 }
 }
 
 
+// FloatFromString creates a new Float from string f.
+// If the string is equal to the value of nullString then the Float will be null.
+// An empty string f will return an error.
+func FloatFromString(f string, nullString string) (Float, error) {
+	if f == nullString {
+		return FloatFromPtr(nil), nil
+	}
+	fV, err := strconv.ParseFloat(f, 64)
+	if err != nil {
+		return Float{}, err
+	}
+	return FloatFrom(fV), nil
+}
+
 // UnmarshalJSON implements json.Unmarshaler.
 // UnmarshalJSON implements json.Unmarshaler.
 // It supports number and null input.
 // It supports number and null input.
 // 0 will not be considered a null Float.
 // 0 will not be considered a null Float.

+ 101 - 37
pkg/tsdb/testdata/scenarios.go

@@ -113,6 +113,14 @@ func init() {
 		Description: PredictablePulseDesc,
 		Description: PredictablePulseDesc,
 	})
 	})
 
 
+	registerScenario(&Scenario{
+		Id:   "predictable_csv_wave",
+		Name: "Predictable CSV Wave",
+		Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+			return getPredictableCSVWave(query, context)
+		},
+	})
+
 	registerScenario(&Scenario{
 	registerScenario(&Scenario{
 		Id:   "random_walk_table",
 		Id:   "random_walk_table",
 		Name: "Random Walk Table",
 		Name: "Random Walk Table",
@@ -385,27 +393,6 @@ func getPredictablePulse(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.Query
 		return queryRes
 		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"))
 	onValue, err = fromStringOrNumber(options.Get("onValue"))
 	if err != nil {
 	if err != nil {
 		queryRes.Error = fmt.Errorf("failed to parse onValue value '%v' into float: %v", options.Get("onValue"), err)
 		queryRes.Error = fmt.Errorf("failed to parse onValue value '%v' into float: %v", options.Get("onValue"), err)
@@ -417,37 +404,99 @@ func getPredictablePulse(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.Query
 		return queryRes
 		return queryRes
 	}
 	}
 
 
-	from := context.TimeRange.GetFromAsMsEpoch()
-	to := context.TimeRange.GetToAsMsEpoch()
+	timeStep = timeStep * 1000                     // Seconds to Milliseconds
+	onFor := func(mod int64) (null.Float, error) { // 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, nil
+			}
+		}
+		return offValue, nil
+	}
+	points, err := predictableSeries(context.TimeRange, timeStep, onCount+offCount, onFor)
+	if err != nil {
+		queryRes.Error = err
+		return queryRes
+	}
 
 
 	series := newSeriesForQuery(query)
 	series := newSeriesForQuery(query)
-	points := make(tsdb.TimeSeriesPoints, 0)
+	series.Points = *points
+	queryRes.Series = append(queryRes.Series, series)
+	return queryRes
+}
 
 
-	timeStep = timeStep * 1000             // Seconds to Milliseconds
-	timeCursor := from - (from % timeStep) // Truncate Start
-	wavePeriod := timeStep * (onCount + offCount)
-	maxPoints := 10000 // Don't return too many points
+func getPredictableCSVWave(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
+	queryRes := tsdb.NewQueryResult()
+
+	// Process Input
+	var timeStep int64
+
+	options := query.Model.Get("csvWave")
+
+	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
+	}
+	rawValues := options.Get("valuesCSV").MustString()
+	rawValues = strings.TrimRight(strings.TrimSpace(rawValues), ",") // Strip Trailing Comma
+	rawValesCSV := strings.Split(rawValues, ",")
+	values := make([]null.Float, len(rawValesCSV))
+	for i, rawValue := range rawValesCSV {
+		val, err := null.FloatFromString(strings.TrimSpace(rawValue), "null")
+		if err != nil {
+			queryRes.Error = fmt.Errorf("failed to parse value '%v' into nullable float: err", rawValue, err)
+			return queryRes
+		}
+		values[i] = val
+	}
 
 
-	onFor := func(mod int64) null.Float { // How many items in the cycle should get the on value
+	timeStep = timeStep * 1000 // Seconds to Milliseconds
+	valuesLen := int64(len(values))
+	getValue := func(mod int64) (null.Float, error) {
 		var i int64
 		var i int64
-		for i = 0; i < onCount; i++ {
+		for i = 0; i < valuesLen; i++ {
 			if mod == i*timeStep {
 			if mod == i*timeStep {
-				return onValue
+				return values[i], nil
 			}
 			}
 		}
 		}
-		return offValue
+		return null.Float{}, fmt.Errorf("did not get value at point in waveform - should not be here")
 	}
 	}
-	for i := 0; i < maxPoints && timeCursor < to; i++ {
-		point := tsdb.NewTimePoint(onFor(timeCursor%wavePeriod), float64(timeCursor))
-		points = append(points, point)
-		timeCursor += timeStep
+	points, err := predictableSeries(context.TimeRange, timeStep, valuesLen, getValue)
+	if err != nil {
+		queryRes.Error = err
+		return queryRes
 	}
 	}
 
 
-	series.Points = points
+	series := newSeriesForQuery(query)
+	series.Points = *points
 	queryRes.Series = append(queryRes.Series, series)
 	queryRes.Series = append(queryRes.Series, series)
 	return queryRes
 	return queryRes
 }
 }
 
 
+func predictableSeries(timeRange *tsdb.TimeRange, timeStep, length int64, getValue func(mod int64) (null.Float, error)) (*tsdb.TimeSeriesPoints, error) {
+	points := make(tsdb.TimeSeriesPoints, 0)
+
+	from := timeRange.GetFromAsMsEpoch()
+	to := timeRange.GetToAsMsEpoch()
+
+	timeCursor := from - (from % timeStep) // Truncate Start
+	wavePeriod := timeStep * length
+	maxPoints := 10000 // Don't return too many points
+
+	for i := 0; i < maxPoints && timeCursor < to; i++ {
+		val, err := getValue(timeCursor % wavePeriod)
+		if err != nil {
+			return &points, err
+		}
+		point := tsdb.NewTimePoint(val, float64(timeCursor))
+		points = append(points, point)
+		timeCursor += timeStep
+	}
+	return &points, nil
+}
+
 func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult {
 func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult {
 	timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch()
 	timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch()
 	to := tsdbQuery.TimeRange.GetToAsMsEpoch()
 	to := tsdbQuery.TimeRange.GetToAsMsEpoch()
@@ -541,3 +590,18 @@ func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries {
 
 
 	return &tsdb.TimeSeries{Name: alias}
 	return &tsdb.TimeSeries{Name: alias}
 }
 }
+
+func fromStringOrNumber(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:
+		return null.FloatFromString(v, "null")
+	default:
+		return null.Float{}, fmt.Errorf("failed to extract value")
+	}
+}

+ 28 - 0
public/app/plugins/datasource/testdata/partials/query.editor.html

@@ -180,4 +180,32 @@
 				ng-model-onblur />
 				ng-model-onblur />
 		</div>
 		</div>
 	</div>
 	</div>
+
+	<!-- Predictable CSV Wave Scenario Options Form -->
+	<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'predictable_csv_wave'">
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword width-7">
+					Step
+					<info-popover mode="right-normal">The number of seconds between datapoints.</info-popover>
+				</label>
+					<input type="number"
+					class="gf-form-input width-5"
+					placeholder="60"
+					ng-model="ctrl.target.csvWave.timeStep"
+					ng-change="ctrl.refresh()"
+					ng-model-onblur />
+			</div>
+			<div class="gf-form gf-form--grow">
+					<label class="gf-form-label query-keyword width-10">
+							CSV Values
+							<info-popover mode="right-normal">Comma separated values. Each value may be an int, float, or null and must not be empty. Whitespace and trailing commas are removed.</info-popover>
+						</label>
+					<input type="string"
+						class="gf-form-input gf-form-label--grow"
+						placeholder="1,2,3,2"
+						ng-model="ctrl.target.csvWave.valuesCSV"
+						ng-change="ctrl.refresh()"
+						ng-model-onblur />
+				</div>
+		</div>
 </query-editor-row>
 </query-editor-row>

+ 11 - 0
public/app/plugins/datasource/testdata/query_ctrl.ts

@@ -13,6 +13,11 @@ export const defaultPulse: any = {
   offValue: 1,
   offValue: 1,
 };
 };
 
 
+export const defaultCSVWave: any = {
+  timeStep: 60,
+  valuesCSV: '0,0,2,2,1,1',
+};
+
 export class TestDataQueryCtrl extends QueryCtrl {
 export class TestDataQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
   static templateUrl = 'partials/query.editor.html';
 
 
@@ -89,6 +94,12 @@ export class TestDataQueryCtrl extends QueryCtrl {
       delete this.target.pulseWave;
       delete this.target.pulseWave;
     }
     }
 
 
+    if (this.target.scenarioId === 'predictable_csv_wave') {
+      this.target.csvWave = _.defaults(this.target.csvWave || {}, defaultCSVWave);
+    } else {
+      delete this.target.csvWave;
+    }
+
     this.refresh();
     this.refresh();
   }
   }