TestData: Change predictable csv scenario to multi-series (#33442)

Allow multiple series in Predictable CSV Wave testdata scenario

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Kyle Brandt 2021-04-27 16:35:43 -04:00 committed by GitHub
parent b590e95682
commit 968935b8b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 213 additions and 112 deletions

View File

@ -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, "=")

View File

@ -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' && <PredictablePulseEditor onChange={onPulseWaveChange} query={query} />}
{scenarioId === 'predictable_csv_wave' && <CSVWaveEditor onChange={onCSVWaveChange} query={query} />}
{scenarioId === 'predictable_csv_wave' && <CSVWavesEditor onChange={onCSVWaveChange} waves={query.csvWave} />}
{scenarioId === 'node_graph' && (
<NodeGraphEditor onChange={(val: NodesQuery) => onChange({ ...query, nodes: val })} query={query} />
)}

View File

@ -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 (
<InlineFieldRow>
{fields.map(({ label, id, type, placeholder, tooltip }, index) => {
const grow = index === fields.length - 1;
return (
<InlineField label={label} labelWidth={14} key={id} tooltip={tooltip} grow={grow}>
<Input
width={grow ? undefined : 32}
type={type}
name={id}
id={`csvWave.${id}-${query.refId}`}
value={query.csvWave?.[id]}
placeholder={placeholder}
onChange={onChange}
/>
</InlineField>
);
})}
</InlineFieldRow>
);
};
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<WaveProps> {
onFieldChange = (field: keyof CSVWave) => (e: ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<InlineFieldRow>
<InlineField
label={'Values'}
grow
tooltip="Comma separated values. Each value may be an int, float, or null and must not be empty. Whitespace and trailing commas are removed"
>
<Input value={wave.valuesCSV} placeholder={'CSV values'} onChange={this.onCSVChange} autoFocus={true} />
</InlineField>
<InlineField label={'Step'} tooltip="The number of seconds between datapoints.">
<Input value={wave.timeStep} type="number" placeholder={'60'} width={6} onChange={this.onTimeStepChange} />
</InlineField>
<InlineField label={'Labels'}>
<Input value={wave.labels} placeholder={'labels'} width={12} onChange={this.onLabelsChange} />
</InlineField>
<InlineField label={'Name'}>
<Input value={wave.name} placeholder={'name'} width={10} onChange={this.onNameChange} />
</InlineField>
<Button icon={last ? 'plus' : 'minus'} variant="secondary" onClick={action} />
</InlineFieldRow>
);
}
}
export class CSVWavesEditor extends PureComponent<WavesProps> {
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) => (
<CSVWaveEditor
key={`${index}/${wave.valuesCSV}`}
wave={wave}
index={index}
onAdd={this.onAdd}
onChange={this.onChange}
last={index === waves.length - 1}
/>
))}
</>
);
}
}

View File

@ -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}
/>

View File

@ -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',

View File

@ -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;
}

View File

@ -9,7 +9,7 @@ export class TestDataVariableSupport extends StandardVariableSupport<TestDataDat
refId: 'TestDataDataSource-QueryVariable',
stringInput: query.query,
scenarioId: 'variables-query',
csvWave: null,
csvWave: undefined,
points: [],
};
}