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 {
|
for _, q := range req.Queries {
|
||||||
model, err := simplejson.NewJson(q.JSON)
|
model, err := simplejson.NewJson(q.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
respD := resp.Responses[q.RefID]
|
respD := resp.Responses[q.RefID]
|
||||||
frame, err := predictableCSVWave(q, model)
|
frames, err := predictableCSVWave(q, model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, err
|
||||||
}
|
}
|
||||||
respD.Frames = append(respD.Frames, frame)
|
respD.Frames = append(respD.Frames, frames...)
|
||||||
resp.Responses[q.RefID] = respD
|
resp.Responses[q.RefID] = respD
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -786,16 +786,31 @@ func randomWalkTable(query backend.DataQuery, model *simplejson.Json) *data.Fram
|
|||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
func predictableCSVWave(query backend.DataQuery, model *simplejson.Json) (*data.Frame, error) {
|
type pCSVOptions struct {
|
||||||
options := model.Get("csvWave")
|
TimeStep int64 `json:"timeStep"`
|
||||||
|
ValuesCSV string `json:"valuesCSV"`
|
||||||
var timeStep int64
|
Labels string `json:"labels"`
|
||||||
var err error
|
Name string `json:"name"`
|
||||||
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
|
func predictableCSVWave(query backend.DataQuery, model *simplejson.Json) ([]*data.Frame, error) {
|
||||||
|
rawQueries, err := model.Get("csvWave").ToDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
queries := []pCSVOptions{}
|
||||||
|
err = json.Unmarshal(rawQueries, &queries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ",")
|
rawValesCSV := strings.Split(rawValues, ",")
|
||||||
values := make([]*float64, len(rawValesCSV))
|
values := make([]*float64, len(rawValesCSV))
|
||||||
|
|
||||||
@ -819,27 +834,31 @@ func predictableCSVWave(query backend.DataQuery, model *simplejson.Json) (*data.
|
|||||||
values[i] = val
|
values[i] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
timeStep *= 1000 // Seconds to Milliseconds
|
subQ.TimeStep *= 1000 // Seconds to Milliseconds
|
||||||
valuesLen := int64(len(values))
|
valuesLen := int64(len(values))
|
||||||
getValue := func(mod int64) (*float64, error) {
|
getValue := func(mod int64) (*float64, error) {
|
||||||
var i int64
|
var i int64
|
||||||
for i = 0; i < valuesLen; i++ {
|
for i = 0; i < valuesLen; i++ {
|
||||||
if mod == i*timeStep {
|
if mod == i*subQ.TimeStep {
|
||||||
return values[i], nil
|
return values[i], nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("did not get value at point in waveform - should not be here")
|
return nil, fmt.Errorf("did not get value at point in waveform - should not be here")
|
||||||
}
|
}
|
||||||
fields, err := predictableSeries(query.TimeRange, timeStep, valuesLen, getValue)
|
fields, err := predictableSeries(query.TimeRange, subQ.TimeStep, valuesLen, getValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := newSeriesForQuery(query, model, 0)
|
frame := newSeriesForQuery(query, model, 0)
|
||||||
frame.Fields = fields
|
frame.Fields = fields
|
||||||
frame.Fields[1].Labels = parseLabels(model)
|
frame.Fields[1].Labels = parseLabelsString(subQ.Labels)
|
||||||
|
if subQ.Name != "" {
|
||||||
return frame, nil
|
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) {
|
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"}`
|
* '{job="foo", instance="bar"} => {job: "foo", instance: "bar"}`
|
||||||
*/
|
*/
|
||||||
func parseLabels(model *simplejson.Json) data.Labels {
|
func parseLabels(model *simplejson.Json) data.Labels {
|
||||||
tags := data.Labels{}
|
|
||||||
|
|
||||||
labelText := model.Get("labels").MustString("")
|
labelText := model.Get("labels").MustString("")
|
||||||
|
return parseLabelsString(labelText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLabelsString(labelText string) data.Labels {
|
||||||
if labelText == "" {
|
if labelText == "" {
|
||||||
return data.Labels{}
|
return data.Labels{}
|
||||||
}
|
}
|
||||||
|
|
||||||
text := strings.Trim(labelText, `{}`)
|
text := strings.Trim(labelText, `{}`)
|
||||||
if len(text) < 2 {
|
if len(text) < 2 {
|
||||||
return tags
|
return data.Labels{}
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = make(data.Labels)
|
tags := make(data.Labels)
|
||||||
|
|
||||||
for _, keyval := range strings.Split(text, ",") {
|
for _, keyval := range strings.Split(text, ",") {
|
||||||
idx := strings.Index(keyval, "=")
|
idx := strings.Index(keyval, "=")
|
||||||
|
@ -10,15 +10,15 @@ import { StreamingClientEditor, ManualEntryEditor, RandomWalkEditor } from './co
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { TestDataDataSource } from './datasource';
|
import { TestDataDataSource } from './datasource';
|
||||||
import { TestDataQuery, Scenario, NodesQuery } from './types';
|
import { TestDataQuery, Scenario, NodesQuery, CSVWave } from './types';
|
||||||
import { PredictablePulseEditor } from './components/PredictablePulseEditor';
|
import { PredictablePulseEditor } from './components/PredictablePulseEditor';
|
||||||
import { CSVWaveEditor } from './components/CSVWaveEditor';
|
import { CSVWavesEditor } from './components/CSVWaveEditor';
|
||||||
import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants';
|
import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants';
|
||||||
import { GrafanaLiveEditor } from './components/GrafanaLiveEditor';
|
import { GrafanaLiveEditor } from './components/GrafanaLiveEditor';
|
||||||
import { NodeGraphEditor } from './components/NodeGraphEditor';
|
import { NodeGraphEditor } from './components/NodeGraphEditor';
|
||||||
import { defaultStreamQuery } from './runStreams';
|
import { defaultStreamQuery } from './runStreams';
|
||||||
|
|
||||||
const showLabelsFor = ['random_walk', 'predictable_pulse', 'predictable_csv_wave'];
|
const showLabelsFor = ['random_walk', 'predictable_pulse'];
|
||||||
const endpoints = [
|
const endpoints = [
|
||||||
{ value: 'datasources', label: 'Data Sources' },
|
{ value: 'datasources', label: 'Data Sources' },
|
||||||
{ value: 'search', label: 'Search' },
|
{ value: 'search', label: 'Search' },
|
||||||
@ -114,7 +114,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
|
|||||||
newValue = Number(value);
|
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) => {
|
const onEndPointChange = ({ value }: SelectableValue) => {
|
||||||
@ -123,7 +123,10 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
|
|||||||
|
|
||||||
const onStreamClientChange = onFieldChange('stream');
|
const onStreamClientChange = onFieldChange('stream');
|
||||||
const onPulseWaveChange = onFieldChange('pulseWave');
|
const onPulseWaveChange = onFieldChange('pulseWave');
|
||||||
const onCSVWaveChange = onFieldChange('csvWave');
|
|
||||||
|
const onCSVWaveChange = (csvWave?: CSVWave[]) => {
|
||||||
|
onUpdate({ ...query, csvWave });
|
||||||
|
};
|
||||||
|
|
||||||
const options = useMemo(
|
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_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' && (
|
{scenarioId === 'node_graph' && (
|
||||||
<NodeGraphEditor onChange={(val: NodesQuery) => onChange({ ...query, nodes: val })} query={query} />
|
<NodeGraphEditor onChange={(val: NodesQuery) => onChange({ ...query, nodes: val })} query={query} />
|
||||||
)}
|
)}
|
||||||
|
@ -1,43 +1,112 @@
|
|||||||
import React from 'react';
|
import React, { ChangeEvent, PureComponent } from 'react';
|
||||||
import { EditorProps } from '../QueryEditor';
|
import { Button, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||||
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
import { CSVWave } from '../types';
|
||||||
|
import { defaultCSVWaveQuery } from '../constants';
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
{fields.map(({ label, id, type, placeholder, tooltip }, index) => {
|
<InlineField
|
||||||
const grow = index === fields.length - 1;
|
label={'Values'}
|
||||||
return (
|
grow
|
||||||
<InlineField label={label} labelWidth={14} key={id} tooltip={tooltip} grow={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
|
>
|
||||||
width={grow ? undefined : 32}
|
<Input value={wave.valuesCSV} placeholder={'CSV values'} onChange={this.onCSVChange} autoFocus={true} />
|
||||||
type={type}
|
|
||||||
name={id}
|
|
||||||
id={`csvWave.${id}-${query.refId}`}
|
|
||||||
value={query.csvWave?.[id]}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
</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>
|
</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}`}
|
id={`randomWalk-${id}-${query.refId}`}
|
||||||
min={min}
|
min={min}
|
||||||
step={step}
|
step={step}
|
||||||
value={query[id as keyof TestDataQuery] || placeholder}
|
value={(query as any)[id as keyof TestDataQuery] || placeholder}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TestDataQuery } from './types';
|
import { CSVWave, TestDataQuery } from './types';
|
||||||
|
|
||||||
export const defaultPulseQuery: any = {
|
export const defaultPulseQuery: any = {
|
||||||
timeStep: 60,
|
timeStep: 60,
|
||||||
@ -8,10 +8,12 @@ export const defaultPulseQuery: any = {
|
|||||||
offValue: 1,
|
offValue: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultCSVWaveQuery: any = {
|
export const defaultCSVWaveQuery: CSVWave[] = [
|
||||||
|
{
|
||||||
timeStep: 60,
|
timeStep: 60,
|
||||||
valuesCSV: '0,0,2,2,1,1',
|
valuesCSV: '0,0,2,2,1,1',
|
||||||
};
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const defaultQuery: TestDataQuery = {
|
export const defaultQuery: TestDataQuery = {
|
||||||
scenarioId: 'random_walk',
|
scenarioId: 'random_walk',
|
||||||
|
@ -21,7 +21,7 @@ export interface TestDataQuery extends DataQuery {
|
|||||||
points?: Points;
|
points?: Points;
|
||||||
stream?: StreamingQuery;
|
stream?: StreamingQuery;
|
||||||
pulseWave?: PulseWaveQuery;
|
pulseWave?: PulseWaveQuery;
|
||||||
csvWave?: any;
|
csvWave?: CSVWave[];
|
||||||
labels?: string;
|
labels?: string;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
levelColumn?: boolean;
|
levelColumn?: boolean;
|
||||||
@ -50,3 +50,9 @@ export interface PulseWaveQuery {
|
|||||||
onValue?: number;
|
onValue?: number;
|
||||||
offValue?: 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',
|
refId: 'TestDataDataSource-QueryVariable',
|
||||||
stringInput: query.query,
|
stringInput: query.query,
|
||||||
scenarioId: 'variables-query',
|
scenarioId: 'variables-query',
|
||||||
csvWave: null,
|
csvWave: undefined,
|
||||||
points: [],
|
points: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user