From 968935b8b70a84e17f4643d18706f9e55ee00035 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 27 Apr 2021 16:35:43 -0400 Subject: [PATCH] TestData: Change predictable csv scenario to multi-series (#33442) Allow multiple series in Predictable CSV Wave testdata scenario Co-authored-by: Ryan McKinley --- pkg/tsdb/testdatasource/scenarios.go | 133 ++++++++------- .../datasource/testdata/QueryEditor.tsx | 15 +- .../testdata/components/CSVWaveEditor.tsx | 153 +++++++++++++----- .../testdata/components/RandomWalkEditor.tsx | 2 +- .../plugins/datasource/testdata/constants.ts | 12 +- .../app/plugins/datasource/testdata/types.ts | 8 +- .../plugins/datasource/testdata/variables.ts | 2 +- 7 files changed, 213 insertions(+), 112 deletions(-) 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 ( + + + + + + + + + + + + + +