diff --git a/pkg/tsdb/testdatasource/scenarios.go b/pkg/tsdb/testdatasource/scenarios.go
index 46f76d21f36..308132acc87 100644
--- a/pkg/tsdb/testdatasource/scenarios.go
+++ b/pkg/tsdb/testdatasource/scenarios.go
@@ -475,15 +475,15 @@ func (p *testDataPlugin) handlePredictableCSVWaveScenario(ctx context.Context, r
for _, q := range req.Queries {
model, err := simplejson.NewJson(q.JSON)
if err != nil {
- continue
+ return nil, err
}
respD := resp.Responses[q.RefID]
- frame, err := predictableCSVWave(q, model)
+ frames, err := predictableCSVWave(q, model)
if err != nil {
- continue
+ return nil, err
}
- respD.Frames = append(respD.Frames, frame)
+ respD.Frames = append(respD.Frames, frames...)
resp.Responses[q.RefID] = respD
}
@@ -786,60 +786,79 @@ func randomWalkTable(query backend.DataQuery, model *simplejson.Json) *data.Fram
return frame
}
-func predictableCSVWave(query backend.DataQuery, model *simplejson.Json) (*data.Frame, error) {
- options := model.Get("csvWave")
+type pCSVOptions struct {
+ TimeStep int64 `json:"timeStep"`
+ ValuesCSV string `json:"valuesCSV"`
+ Labels string `json:"labels"`
+ Name string `json:"name"`
+}
- var timeStep int64
- var err error
- if timeStep, err = options.Get("timeStep").Int64(); err != nil {
- return nil, fmt.Errorf("failed to parse timeStep value '%v' into integer: %v", options.Get("timeStep"), err)
- }
- rawValues := options.Get("valuesCSV").MustString()
- rawValues = strings.TrimRight(strings.TrimSpace(rawValues), ",") // Strip Trailing Comma
- rawValesCSV := strings.Split(rawValues, ",")
- values := make([]*float64, len(rawValesCSV))
-
- for i, rawValue := range rawValesCSV {
- var val *float64
- rawValue = strings.TrimSpace(rawValue)
-
- switch rawValue {
- case "null":
- // val stays nil
- case "nan":
- f := math.NaN()
- val = &f
- default:
- f, err := strconv.ParseFloat(rawValue, 64)
- if err != nil {
- return nil, errutil.Wrapf(err, "failed to parse value '%v' into nullable float", rawValue)
- }
- val = &f
- }
- values[i] = val
- }
-
- timeStep *= 1000 // Seconds to Milliseconds
- valuesLen := int64(len(values))
- getValue := func(mod int64) (*float64, error) {
- var i int64
- for i = 0; i < valuesLen; i++ {
- if mod == i*timeStep {
- return values[i], nil
- }
- }
- return nil, fmt.Errorf("did not get value at point in waveform - should not be here")
- }
- fields, err := predictableSeries(query.TimeRange, timeStep, valuesLen, getValue)
+func predictableCSVWave(query backend.DataQuery, model *simplejson.Json) ([]*data.Frame, error) {
+ rawQueries, err := model.Get("csvWave").ToDB()
if err != nil {
return nil, err
}
- frame := newSeriesForQuery(query, model, 0)
- frame.Fields = fields
- frame.Fields[1].Labels = parseLabels(model)
+ queries := []pCSVOptions{}
+ err = json.Unmarshal(rawQueries, &queries)
+ if err != nil {
+ return nil, err
+ }
- return frame, nil
+ frames := make([]*data.Frame, 0, len(queries))
+
+ for _, subQ := range queries {
+ var err error
+
+ rawValues := strings.TrimRight(strings.TrimSpace(subQ.ValuesCSV), ",") // Strip Trailing Comma
+ rawValesCSV := strings.Split(rawValues, ",")
+ values := make([]*float64, len(rawValesCSV))
+
+ for i, rawValue := range rawValesCSV {
+ var val *float64
+ rawValue = strings.TrimSpace(rawValue)
+
+ switch rawValue {
+ case "null":
+ // val stays nil
+ case "nan":
+ f := math.NaN()
+ val = &f
+ default:
+ f, err := strconv.ParseFloat(rawValue, 64)
+ if err != nil {
+ return nil, errutil.Wrapf(err, "failed to parse value '%v' into nullable float", rawValue)
+ }
+ val = &f
+ }
+ values[i] = val
+ }
+
+ subQ.TimeStep *= 1000 // Seconds to Milliseconds
+ valuesLen := int64(len(values))
+ getValue := func(mod int64) (*float64, error) {
+ var i int64
+ for i = 0; i < valuesLen; i++ {
+ if mod == i*subQ.TimeStep {
+ return values[i], nil
+ }
+ }
+ return nil, fmt.Errorf("did not get value at point in waveform - should not be here")
+ }
+ fields, err := predictableSeries(query.TimeRange, subQ.TimeStep, valuesLen, getValue)
+ if err != nil {
+ return nil, err
+ }
+
+ frame := newSeriesForQuery(query, model, 0)
+ frame.Fields = fields
+ frame.Fields[1].Labels = parseLabelsString(subQ.Labels)
+ if subQ.Name != "" {
+ frame.Name = subQ.Name
+ }
+ frames = append(frames, frame)
+ }
+ return frames, nil
}
func predictableSeries(timeRange backend.TimeRange, timeStep, length int64, getValue func(mod int64) (*float64, error)) (data.Fields, error) {
@@ -988,19 +1007,21 @@ func newSeriesForQuery(query backend.DataQuery, model *simplejson.Json, index in
* '{job="foo", instance="bar"} => {job: "foo", instance: "bar"}`
*/
func parseLabels(model *simplejson.Json) data.Labels {
- tags := data.Labels{}
-
labelText := model.Get("labels").MustString("")
+ return parseLabelsString(labelText)
+}
+
+func parseLabelsString(labelText string) data.Labels {
if labelText == "" {
return data.Labels{}
}
text := strings.Trim(labelText, `{}`)
if len(text) < 2 {
- return tags
+ return data.Labels{}
}
- tags = make(data.Labels)
+ tags := make(data.Labels)
for _, keyval := range strings.Split(text, ",") {
idx := strings.Index(keyval, "=")
diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx
index f91fdbfb372..1d31fcf9b93 100644
--- a/public/app/plugins/datasource/testdata/QueryEditor.tsx
+++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx
@@ -10,15 +10,15 @@ import { StreamingClientEditor, ManualEntryEditor, RandomWalkEditor } from './co
// Types
import { TestDataDataSource } from './datasource';
-import { TestDataQuery, Scenario, NodesQuery } from './types';
+import { TestDataQuery, Scenario, NodesQuery, CSVWave } from './types';
import { PredictablePulseEditor } from './components/PredictablePulseEditor';
-import { CSVWaveEditor } from './components/CSVWaveEditor';
+import { CSVWavesEditor } from './components/CSVWaveEditor';
import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants';
import { GrafanaLiveEditor } from './components/GrafanaLiveEditor';
import { NodeGraphEditor } from './components/NodeGraphEditor';
import { defaultStreamQuery } from './runStreams';
-const showLabelsFor = ['random_walk', 'predictable_pulse', 'predictable_csv_wave'];
+const showLabelsFor = ['random_walk', 'predictable_pulse'];
const endpoints = [
{ value: 'datasources', label: 'Data Sources' },
{ value: 'search', label: 'Search' },
@@ -114,7 +114,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
newValue = Number(value);
}
- onUpdate({ ...query, [field]: { ...query[field as keyof TestDataQuery], [name]: newValue } });
+ onUpdate({ ...query, [field]: { ...(query as any)[field], [name]: newValue } });
};
const onEndPointChange = ({ value }: SelectableValue) => {
@@ -123,7 +123,10 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
const onStreamClientChange = onFieldChange('stream');
const onPulseWaveChange = onFieldChange('pulseWave');
- const onCSVWaveChange = onFieldChange('csvWave');
+
+ const onCSVWaveChange = (csvWave?: CSVWave[]) => {
+ onUpdate({ ...query, csvWave });
+ };
const options = useMemo(
() =>
@@ -248,7 +251,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
)}
{scenarioId === 'predictable_pulse' && }
- {scenarioId === 'predictable_csv_wave' && }
+ {scenarioId === 'predictable_csv_wave' && }
{scenarioId === 'node_graph' && (
onChange({ ...query, nodes: val })} query={query} />
)}
diff --git a/public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx b/public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx
index a480d726519..35864914139 100644
--- a/public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx
+++ b/public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx
@@ -1,43 +1,112 @@
-import React from 'react';
-import { EditorProps } from '../QueryEditor';
-import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
+import React, { ChangeEvent, PureComponent } from 'react';
+import { Button, InlineField, InlineFieldRow, Input } from '@grafana/ui';
+import { CSVWave } from '../types';
+import { defaultCSVWaveQuery } from '../constants';
-const fields = [
- {
- label: 'Step',
- type: 'number',
- id: 'timeStep',
- placeholder: '60',
- tooltip: 'The number of seconds between datapoints.',
- },
- {
- label: 'CSV Values',
- type: 'text',
- id: 'valuesCSV',
- placeholder: '1,2,3,4',
- tooltip:
- 'Comma separated values. Each value may be an int, float, or null and must not be empty. Whitespace and trailing commas are removed.',
- },
-];
-export const CSVWaveEditor = ({ onChange, query }: EditorProps) => {
- return (
-
- {fields.map(({ label, id, type, placeholder, tooltip }, index) => {
- const grow = index === fields.length - 1;
- return (
-
-
-
- );
- })}
-
- );
-};
+interface WavesProps {
+ waves?: CSVWave[];
+ onChange: (waves: CSVWave[]) => void;
+}
+
+interface WaveProps {
+ wave: CSVWave;
+ index: number;
+ last: boolean;
+ onChange: (index: number, wave?: CSVWave) => void;
+ onAdd: () => void;
+}
+
+class CSVWaveEditor extends PureComponent {
+ onFieldChange = (field: keyof CSVWave) => (e: ChangeEvent) => {
+ const { value } = e.target as HTMLInputElement;
+
+ this.props.onChange(this.props.index, {
+ ...this.props.wave,
+ [field]: value,
+ });
+ };
+
+ onNameChange = this.onFieldChange('name');
+ onLabelsChange = this.onFieldChange('labels');
+ onCSVChange = this.onFieldChange('valuesCSV');
+ onTimeStepChange = (e: ChangeEvent) => {
+ const timeStep = e.target.valueAsNumber;
+ this.props.onChange(this.props.index, {
+ ...this.props.wave,
+ timeStep,
+ });
+ };
+
+ render() {
+ const { wave, last } = this.props;
+ let action = this.props.onAdd;
+ if (!last) {
+ action = () => {
+ this.props.onChange(this.props.index, undefined); // remove
+ };
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export class CSVWavesEditor extends PureComponent {
+ onChange = (index: number, wave?: CSVWave) => {
+ let waves = [...(this.props.waves ?? defaultCSVWaveQuery)];
+ if (wave) {
+ waves[index] = { ...wave };
+ } else {
+ // remove the element
+ waves.splice(index, 1);
+ }
+ this.props.onChange(waves);
+ };
+
+ onAdd = () => {
+ const waves = [...(this.props.waves ?? defaultCSVWaveQuery)];
+ waves.push({ ...defaultCSVWaveQuery[0] });
+ this.props.onChange(waves);
+ };
+
+ render() {
+ let waves = this.props.waves ?? defaultCSVWaveQuery;
+ if (!waves.length) {
+ waves = defaultCSVWaveQuery;
+ }
+
+ return (
+ <>
+ {waves.map((wave, index) => (
+
+ ))}
+ >
+ );
+ }
+}
diff --git a/public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx b/public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx
index 79fe23c26f9..42353b02708 100644
--- a/public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx
+++ b/public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx
@@ -30,7 +30,7 @@ export const RandomWalkEditor = ({ onChange, query }: EditorProps) => {
id={`randomWalk-${id}-${query.refId}`}
min={min}
step={step}
- value={query[id as keyof TestDataQuery] || placeholder}
+ value={(query as any)[id as keyof TestDataQuery] || placeholder}
placeholder={placeholder}
onChange={onChange}
/>
diff --git a/public/app/plugins/datasource/testdata/constants.ts b/public/app/plugins/datasource/testdata/constants.ts
index e45a7f99a57..593c9e0656e 100644
--- a/public/app/plugins/datasource/testdata/constants.ts
+++ b/public/app/plugins/datasource/testdata/constants.ts
@@ -1,4 +1,4 @@
-import { TestDataQuery } from './types';
+import { CSVWave, TestDataQuery } from './types';
export const defaultPulseQuery: any = {
timeStep: 60,
@@ -8,10 +8,12 @@ export const defaultPulseQuery: any = {
offValue: 1,
};
-export const defaultCSVWaveQuery: any = {
- timeStep: 60,
- valuesCSV: '0,0,2,2,1,1',
-};
+export const defaultCSVWaveQuery: CSVWave[] = [
+ {
+ timeStep: 60,
+ valuesCSV: '0,0,2,2,1,1',
+ },
+];
export const defaultQuery: TestDataQuery = {
scenarioId: 'random_walk',
diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts
index 8be83b5b047..25b241faf14 100644
--- a/public/app/plugins/datasource/testdata/types.ts
+++ b/public/app/plugins/datasource/testdata/types.ts
@@ -21,7 +21,7 @@ export interface TestDataQuery extends DataQuery {
points?: Points;
stream?: StreamingQuery;
pulseWave?: PulseWaveQuery;
- csvWave?: any;
+ csvWave?: CSVWave[];
labels?: string;
lines?: number;
levelColumn?: boolean;
@@ -50,3 +50,9 @@ export interface PulseWaveQuery {
onValue?: number;
offValue?: number;
}
+export interface CSVWave {
+ timeStep?: number;
+ name?: string;
+ valuesCSV?: string;
+ labels?: string;
+}
diff --git a/public/app/plugins/datasource/testdata/variables.ts b/public/app/plugins/datasource/testdata/variables.ts
index d0d6b5783e3..a11f0fc2a41 100644
--- a/public/app/plugins/datasource/testdata/variables.ts
+++ b/public/app/plugins/datasource/testdata/variables.ts
@@ -9,7 +9,7 @@ export class TestDataVariableSupport extends StandardVariableSupport