mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
b590e95682
commit
968935b8b7
@ -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, "=")
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export class TestDataVariableSupport extends StandardVariableSupport<TestDataDat
|
||||
refId: 'TestDataDataSource-QueryVariable',
|
||||
stringInput: query.query,
|
||||
scenarioId: 'variables-query',
|
||||
csvWave: null,
|
||||
csvWave: undefined,
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user