mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Migration: TestData Query Editor (#27997)
* Use new editor * Add basic fields * Add labels * Add ids * Add ManualEntryEditor * Use tooltip prop * Switch to inline labels * Fix inline label tooltip position * Allow resetting max-width * Replace form inputs * Add random walk editor * separate editors * Add logs and endpoints fields * Add PredictablePulseEditor * Add CSVWaveEditor * Add grow prop * Add default query * Fix types * Fix manual editor * Fix type issues * Handle scenario change * Sort scenarios by label * Add ManualEditor test * Fix label height * test manual editor * Update test * Setup QueryEditor tests * Fix selected value * Connect CSVWaveEditor * Convert stream data to numbers * Fix random walk editor * Cleanup * Convert scenarios to ts * Remove extra icon styles * Minor tweaks * Update e2e tests * Remove useEffect * Add missing aria-labels * Use new button components
This commit is contained in:
parent
b10e39a7e1
commit
3225b119d4
@ -6,4 +6,4 @@ node_modules
|
||||
public/vendor/
|
||||
vendor/
|
||||
data/
|
||||
|
||||
e2e/tmp
|
||||
|
@ -11,7 +11,18 @@ export const smokeTestScenario = {
|
||||
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
|
||||
e2e.pages.AddDashboard.addNewPanel().click();
|
||||
|
||||
e2e.components.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
|
||||
e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.contains('CSV Metric Values')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
});
|
||||
|
||||
// Make sure the graph renders via checking legend
|
||||
e2e.components.Panels.Visualization.Graph.Legend.legendItemAlias('A-series').should('be.visible');
|
||||
|
@ -69,9 +69,20 @@ e2e.scenario({
|
||||
});
|
||||
|
||||
// Change to CSV Metric Values scenario for A
|
||||
e2e.components.DataSource.TestData.QueryTab.scenarioSelect()
|
||||
.eq(1)
|
||||
.select('CSV Metric Values');
|
||||
e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
e2e.components.Select.input()
|
||||
.eq(0)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.contains('CSV Metric Values')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.eq(0)
|
||||
.click();
|
||||
});
|
||||
|
||||
e2e().wait('@apiPostQuery');
|
||||
|
||||
|
@ -4,6 +4,7 @@ export const Components = {
|
||||
DataSource: {
|
||||
TestData: {
|
||||
QueryTab: {
|
||||
scenarioSelectContainer: 'Test Data Query scenario select container',
|
||||
scenarioSelect: 'Test Data Query scenario select',
|
||||
max: 'TestData max',
|
||||
min: 'TestData min',
|
||||
|
80
public/app/plugins/datasource/testdata/QueryEditor.test.tsx
vendored
Normal file
80
public/app/plugins/datasource/testdata/QueryEditor.test.tsx
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { defaultQuery } from './constants';
|
||||
import { QueryEditor, Props } from './QueryEditor';
|
||||
import { scenarios } from './__mocks__/scenarios';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
const props = {
|
||||
onRunQuery: jest.fn(),
|
||||
query: defaultQuery,
|
||||
onChange: mockOnChange,
|
||||
datasource: {
|
||||
getScenarios: () => Promise.resolve(scenarios),
|
||||
} as any,
|
||||
};
|
||||
|
||||
const setup = (testProps?: Partial<Props>) => {
|
||||
const editorProps = { ...props, ...testProps };
|
||||
return render(<QueryEditor {...editorProps} />);
|
||||
};
|
||||
|
||||
describe('Test Datasource Query Editor', () => {
|
||||
it('should render with default scenario', async () => {
|
||||
setup();
|
||||
|
||||
expect(await screen.findByText(/random walk/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Alias' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Labels' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch scenario and display its default values', async () => {
|
||||
const { rerender } = setup();
|
||||
|
||||
let select = (await screen.findByText('Scenario')).nextSibling!;
|
||||
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||
const scs = screen.getAllByLabelText('Select option');
|
||||
|
||||
expect(scs).toHaveLength(scenarios.length);
|
||||
|
||||
await userEvent.click(screen.getByText('CSV Metric Values'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ scenarioId: 'csv_metric_values' }));
|
||||
await rerender(
|
||||
<QueryEditor
|
||||
{...props}
|
||||
query={{ ...defaultQuery, scenarioId: 'csv_metric_values', stringInput: '1,20,90,30,5,0' }}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('textbox', { name: /string input/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /string input/i })).toHaveValue('1,20,90,30,5,0');
|
||||
|
||||
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||
await userEvent.click(screen.getByText('Grafana API'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scenarioId: 'grafana_api', stringInput: 'datasources' })
|
||||
);
|
||||
rerender(
|
||||
<QueryEditor {...props} query={{ ...defaultQuery, scenarioId: 'grafana_api', stringInput: 'datasources' }} />
|
||||
);
|
||||
expect(await screen.findByText('Grafana API')).toBeInTheDocument();
|
||||
expect(screen.getByText('Data Sources')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||
await userEvent.click(screen.getByText('Streaming Client'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ scenarioId: 'streaming_client', stringInput: '' })
|
||||
);
|
||||
rerender(<QueryEditor {...props} query={{ ...defaultQuery, scenarioId: 'streaming_client', stringInput: '' }} />);
|
||||
expect(await screen.findByText('Streaming Client')).toBeInTheDocument();
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Noise')).toHaveValue(2.2);
|
||||
expect(screen.getByLabelText('Speed (ms)')).toHaveValue(250);
|
||||
expect(screen.getByLabelText('Spread')).toHaveValue(3.5);
|
||||
expect(screen.getByLabelText('Bands')).toHaveValue(1);
|
||||
});
|
||||
});
|
@ -1,67 +1,231 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Services & Utils
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import React, { ChangeEvent, FormEvent, useMemo, useEffect } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
// Components
|
||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
||||
const { Select } = LegacyForms;
|
||||
import { selectors as editorSelectors } from '@grafana/e2e-selectors';
|
||||
import { Input, InlineFieldRow, InlineField, Select, TextArea, Switch } from '@grafana/ui';
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { StreamingClientEditor, ManualEntryEditor, RandomWalkEditor } from './components';
|
||||
|
||||
// Types
|
||||
import { TestDataDataSource } from './datasource';
|
||||
import { TestDataQuery, Scenario } from './types';
|
||||
import { PredictablePulseEditor } from './components/PredictablePulseEditor';
|
||||
import { CSVWaveEditor } from './components/CSVWaveEditor';
|
||||
import { defaultQuery } from './constants';
|
||||
|
||||
interface State {
|
||||
scenarioList: Scenario[];
|
||||
current: Scenario | null;
|
||||
const showLabelsFor = ['random_walk', 'predictable_pulse', 'predictable_csv_wave'];
|
||||
const endpoints = [
|
||||
{ value: 'datasources', label: 'Data Sources' },
|
||||
{ value: 'search', label: 'Search' },
|
||||
{ value: 'annotations', label: 'Annotations' },
|
||||
];
|
||||
|
||||
// Fields that need to be transformed to numbers
|
||||
const numberFields = ['lines', 'seriesCount', 'timeStep'];
|
||||
|
||||
const selectors = editorSelectors.components.DataSource.TestData.QueryTab;
|
||||
|
||||
export interface EditorProps {
|
||||
onChange: (value: any) => void;
|
||||
query: TestDataQuery;
|
||||
}
|
||||
|
||||
type Props = QueryEditorProps<TestDataDataSource, TestDataQuery>;
|
||||
export type Props = QueryEditorProps<TestDataDataSource, TestDataQuery>;
|
||||
|
||||
export class QueryEditor extends PureComponent<Props> {
|
||||
backendSrv = getBackendSrv();
|
||||
export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) => {
|
||||
query = { ...defaultQuery, ...query };
|
||||
|
||||
state: State = {
|
||||
scenarioList: [],
|
||||
current: null,
|
||||
const { loading, value: scenarioList } = useAsync<Scenario[]>(async () => {
|
||||
return datasource.getScenarios();
|
||||
}, []);
|
||||
|
||||
const onUpdate = (query: TestDataQuery) => {
|
||||
onChange(query);
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { query, datasource } = this.props;
|
||||
useEffect(() => {
|
||||
onUpdate(query);
|
||||
}, []);
|
||||
|
||||
query.scenarioId = query.scenarioId || 'random_walk';
|
||||
const currentScenario = useMemo(() => scenarioList?.find(scenario => scenario.id === query.scenarioId), [
|
||||
scenarioList,
|
||||
query,
|
||||
]);
|
||||
const scenarioId = currentScenario?.id;
|
||||
|
||||
// const scenarioList = await backendSrv.get('/api/tsdb/testdata/scenarios');
|
||||
const scenarioList = await datasource.getScenarios();
|
||||
const current: any = _.find(scenarioList, { id: query.scenarioId });
|
||||
const onScenarioChange = (item: SelectableValue<string>) => {
|
||||
const scenario = scenarioList?.find(sc => sc.id === item.value);
|
||||
|
||||
this.setState({ scenarioList: scenarioList, current: current });
|
||||
}
|
||||
if (!scenario) {
|
||||
return;
|
||||
}
|
||||
|
||||
onScenarioChange = (item: SelectableValue<string>) => {
|
||||
this.props.onChange({
|
||||
...this.props.query,
|
||||
let stringInput = scenario.stringInput ?? '';
|
||||
|
||||
if (scenario.id === 'grafana_api') {
|
||||
stringInput = 'datasources';
|
||||
}
|
||||
|
||||
onUpdate({
|
||||
...query,
|
||||
scenarioId: item.value!,
|
||||
stringInput,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query } = this.props;
|
||||
const options = this.state.scenarioList.map(item => ({ label: item.name, value: item.id }));
|
||||
const current = options.find(item => item.value === query.scenarioId);
|
||||
const onInputChange = (e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target as HTMLInputElement | HTMLTextAreaElement;
|
||||
let newValue: Partial<TestDataQuery> = { [name]: value };
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="query-keyword" width={7}>
|
||||
Scenario
|
||||
</InlineFormLabel>
|
||||
<Select options={options} value={current} onChange={this.onScenarioChange} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (name === 'levelColumn') {
|
||||
newValue = { levelColumn: (e.target as HTMLInputElement).checked };
|
||||
} else if (numberFields.includes(name)) {
|
||||
newValue = { [name]: Number(value) };
|
||||
}
|
||||
|
||||
onUpdate({ ...query, ...newValue });
|
||||
};
|
||||
|
||||
const onFieldChange = (field: string) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target as HTMLInputElement;
|
||||
const formattedValue = numberFields.includes(name) ? Number(value) : value;
|
||||
onUpdate({ ...query, [field]: { ...query[field as keyof TestDataQuery], [name]: formattedValue } });
|
||||
};
|
||||
|
||||
const onEndPointChange = ({ value }: SelectableValue) => {
|
||||
onUpdate({ ...query, stringInput: value });
|
||||
};
|
||||
|
||||
const onStreamClientChange = onFieldChange('stream');
|
||||
const onPulseWaveChange = onFieldChange('pulseWave');
|
||||
const onCSVWaveChange = onFieldChange('csvWave');
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
(scenarioList || [])
|
||||
.map(item => ({ label: item.name, value: item.id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
[scenarioList]
|
||||
);
|
||||
const showLabels = useMemo(() => showLabelsFor.includes(query.scenarioId), [query]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow aria-label={selectors.scenarioSelectContainer}>
|
||||
<InlineField labelWidth={14} label="Scenario">
|
||||
<Select
|
||||
options={options}
|
||||
value={options.find(item => item.value === query.scenarioId)}
|
||||
onChange={onScenarioChange}
|
||||
width={32}
|
||||
/>
|
||||
</InlineField>
|
||||
{currentScenario?.stringInput && (
|
||||
<InlineField label="String Input">
|
||||
<Input
|
||||
width={32}
|
||||
id="stringInput"
|
||||
name="stringInput"
|
||||
placeholder={query.stringInput}
|
||||
value={query.stringInput}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
<InlineField label="Alias" labelWidth={14}>
|
||||
<Input
|
||||
width={32}
|
||||
id="alias"
|
||||
type="text"
|
||||
placeholder="optional"
|
||||
pattern='[^<>&\\"]+'
|
||||
name="alias"
|
||||
value={query.alias}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</InlineField>
|
||||
{showLabels && (
|
||||
<InlineField
|
||||
label="Labels"
|
||||
labelWidth={14}
|
||||
tooltip={
|
||||
<>
|
||||
Set labels using a key=value syntax:
|
||||
<br />
|
||||
{`{ key = "value", key2 = "value" }`}
|
||||
<br />
|
||||
key="value", key2="value"
|
||||
<br />
|
||||
key=value, key2=value
|
||||
<br />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
width={32}
|
||||
id="labels"
|
||||
name="labels"
|
||||
onChange={onInputChange}
|
||||
value={query?.labels}
|
||||
placeholder="key=value, key2=value2"
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
|
||||
{scenarioId === 'manual_entry' && <ManualEntryEditor onChange={onUpdate} query={query} onRunQuery={onRunQuery} />}
|
||||
{scenarioId === 'random_walk' && <RandomWalkEditor onChange={onInputChange} query={query} />}
|
||||
{scenarioId === 'streaming_client' && <StreamingClientEditor onChange={onStreamClientChange} query={query} />}
|
||||
{scenarioId === 'logs' && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Lines" labelWidth={14}>
|
||||
<Input
|
||||
type="number"
|
||||
name="lines"
|
||||
value={query.lines}
|
||||
width={32}
|
||||
onChange={onInputChange}
|
||||
placeholder="10"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Level" labelWidth={14}>
|
||||
<Switch onChange={onInputChange} name="levelColumn" value={!!query.levelColumn} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
{scenarioId === 'grafana_api' && (
|
||||
<InlineField labelWidth={14} label="Endpoint">
|
||||
<Select
|
||||
options={endpoints}
|
||||
onChange={onEndPointChange}
|
||||
width={32}
|
||||
value={endpoints.find(ep => ep.value === query.stringInput)}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
|
||||
{scenarioId === 'arrow' && (
|
||||
<InlineField grow>
|
||||
<TextArea
|
||||
name="stringInput"
|
||||
value={query.stringInput}
|
||||
rows={10}
|
||||
placeholder="Copy base64 text data from query result"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
|
||||
{scenarioId === 'predictable_pulse' && <PredictablePulseEditor onChange={onPulseWaveChange} query={query} />}
|
||||
{scenarioId === 'predictable_csv_wave' && <CSVWaveEditor onChange={onCSVWaveChange} query={query} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
117
public/app/plugins/datasource/testdata/__mocks__/scenarios.ts
vendored
Normal file
117
public/app/plugins/datasource/testdata/__mocks__/scenarios.ts
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
export const scenarios = [
|
||||
{
|
||||
description: '',
|
||||
id: 'annotations',
|
||||
name: 'Annotations',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'arrow',
|
||||
name: 'Load Apache Arrow Data',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'csv_metric_values',
|
||||
name: 'CSV Metric Values',
|
||||
stringInput: '1,20,90,30,5,0',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'datapoints_outside_range',
|
||||
name: 'Datapoints Outside Range',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'exponential_heatmap_bucket_data',
|
||||
name: 'Exponential heatmap bucket data',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'grafana_api',
|
||||
name: 'Grafana API',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'linear_heatmap_bucket_data',
|
||||
name: 'Linear heatmap bucket data',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'logs',
|
||||
name: 'Logs',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'manual_entry',
|
||||
name: 'Manual Entry',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'no_data_points',
|
||||
name: 'No Data Points',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'predictable_csv_wave',
|
||||
name: 'Predictable CSV Wave',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Predictable Pulse returns a pulse wave where there is a datapoint every timeStepSeconds.\nThe wave cycles at timeStepSeconds*(onCount+offCount).\nThe cycle of the wave is based off of absolute time (from the epoch) which makes it predictable.\nTimestamps will line up evenly on timeStepSeconds (For example, 60 seconds means times will all end in :00 seconds).',
|
||||
id: 'predictable_pulse',
|
||||
name: 'Predictable Pulse',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'random_walk',
|
||||
name: 'Random Walk',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'random_walk_table',
|
||||
name: 'Random Walk Table',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'random_walk_with_error',
|
||||
name: 'Random Walk (with error)',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'server_error_500',
|
||||
name: 'Server Error (500)',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'slow_query',
|
||||
name: 'Slow Query',
|
||||
stringInput: '5s',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'streaming_client',
|
||||
name: 'Streaming Client',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'table_static',
|
||||
name: 'Table Static',
|
||||
stringInput: '',
|
||||
},
|
||||
];
|
43
public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx
vendored
Normal file
43
public/app/plugins/datasource/testdata/components/CSVWaveEditor.tsx
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { EditorProps } from '../QueryEditor';
|
||||
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
|
||||
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}`}
|
||||
value={query.csvWave?.[id]}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
})}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
87
public/app/plugins/datasource/testdata/components/ManualEntryEditor.test.tsx
vendored
Normal file
87
public/app/plugins/datasource/testdata/components/ManualEntryEditor.test.tsx
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ManualEntryEditor, Props } from './ManualEntryEditor';
|
||||
import { defaultQuery } from '../constants';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
const setup = (testProps?: Partial<Props>) => {
|
||||
const props = {
|
||||
onRunQuery: jest.fn(),
|
||||
query: defaultQuery,
|
||||
onChange: mockOnChange,
|
||||
...testProps,
|
||||
};
|
||||
|
||||
return render(<ManualEntryEditor {...props} />);
|
||||
};
|
||||
|
||||
describe('ManualEntryEditor', () => {
|
||||
it('should render', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByLabelText(/New value/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Time/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Add/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/select point/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add new point', async () => {
|
||||
setup();
|
||||
|
||||
userEvent.type(screen.getByLabelText(/New value/i), '10');
|
||||
userEvent.clear(screen.getByLabelText(/Time/i));
|
||||
userEvent.type(screen.getByLabelText(/Time/i), '2020-11-01T14:19:30+00:00');
|
||||
userEvent.click(screen.getByRole('button', { name: /Add/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ points: [[10, 1604240370000]] }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should list selected points and delete selected ones', async () => {
|
||||
const editor = setup({
|
||||
query: {
|
||||
...defaultQuery,
|
||||
points: [
|
||||
[10, 1604240370000],
|
||||
[15, 1604340370000],
|
||||
],
|
||||
},
|
||||
});
|
||||
let select = screen.getByText('All values').nextSibling!;
|
||||
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||
const points = screen.getAllByLabelText('Select option');
|
||||
expect(points).toHaveLength(2);
|
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(points[0]);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ points: [[15, 1604340370000]] }));
|
||||
});
|
||||
|
||||
editor.rerender(
|
||||
<ManualEntryEditor
|
||||
query={{
|
||||
...defaultQuery,
|
||||
points: [[15, 1604340370000]],
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
onRunQuery={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
select = screen.getByText('All values').nextSibling!;
|
||||
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||
expect(screen.getAllByLabelText('Select option')).toHaveLength(1);
|
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
94
public/app/plugins/datasource/testdata/components/ManualEntryEditor.tsx
vendored
Normal file
94
public/app/plugins/datasource/testdata/components/ManualEntryEditor.tsx
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { dateMath, dateTime, SelectableValue } from '@grafana/data';
|
||||
import { Form, InlineField, InlineFieldRow, Input, InputControl, Select, Button } from '@grafana/ui';
|
||||
import { EditorProps } from '../QueryEditor';
|
||||
import { NewPoint } from '../types';
|
||||
|
||||
export interface Props extends EditorProps {
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => {
|
||||
const addPoint = (point: NewPoint) => {
|
||||
const newPointTime = dateMath.parse(point.newPointTime);
|
||||
const points = [...query.points, [Number(point.newPointValue), newPointTime!.valueOf()]].sort(
|
||||
(a, b) => a[1] - b[1]
|
||||
);
|
||||
onChange({ ...query, points });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const deletePoint = (point: SelectableValue) => {
|
||||
const points = query.points.filter((_, index) => index !== point.value);
|
||||
onChange({ ...query, points });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const points = query.points.map((point, index) => {
|
||||
return {
|
||||
label: dateTime(point[1]).format('MMMM Do YYYY, H:mm:ss') + ' : ' + point[0],
|
||||
value: index,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Form onSubmit={addPoint} maxWidth="none">
|
||||
{({ register, control, watch }) => {
|
||||
const selectedPoint = watch('selectedPoint') as SelectableValue;
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="New value" labelWidth={14}>
|
||||
<Input
|
||||
width={32}
|
||||
type="number"
|
||||
placeholder="value"
|
||||
id="newPointValue"
|
||||
name="newPointValue"
|
||||
ref={register}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Time" labelWidth={14}>
|
||||
<Input
|
||||
width={32}
|
||||
id="newPointTime"
|
||||
placeholder="time"
|
||||
name="newPointTime"
|
||||
ref={register}
|
||||
defaultValue={dateTime().format()}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField>
|
||||
<Button variant="secondary">Add</Button>
|
||||
</InlineField>
|
||||
<InlineField label="All values">
|
||||
<InputControl
|
||||
control={control}
|
||||
as={Select}
|
||||
options={points}
|
||||
width={32}
|
||||
name="selectedPoint"
|
||||
onChange={value => value[0]}
|
||||
placeholder="Select point"
|
||||
/>
|
||||
</InlineField>
|
||||
|
||||
{selectedPoint?.value !== undefined && (
|
||||
<InlineField>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
control.setValue('selectedPoint', [{ value: undefined, label: 'Select value' }]);
|
||||
deletePoint(selectedPoint);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</InlineField>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
56
public/app/plugins/datasource/testdata/components/PredictablePulseEditor.tsx
vendored
Normal file
56
public/app/plugins/datasource/testdata/components/PredictablePulseEditor.tsx
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { EditorProps } from '../QueryEditor';
|
||||
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import { PulseWaveQuery } from '../types';
|
||||
|
||||
const fields = [
|
||||
{ label: 'Step', id: 'timeStep', placeholder: '60', tooltip: 'The number of seconds between datapoints.' },
|
||||
{
|
||||
label: 'On Count',
|
||||
id: 'onCount',
|
||||
placeholder: '3',
|
||||
tooltip: 'The number of values within a cycle, at the start of the cycle, that should have the onValue.',
|
||||
},
|
||||
{ label: 'Off Count', id: 'offCount', placeholder: '6', tooltip: 'The number of offValues within the cycle.' },
|
||||
{
|
||||
label: 'On Value',
|
||||
id: 'onValue',
|
||||
placeholder: '1',
|
||||
tooltip: 'The value for "on values", may be an int, float, or null.',
|
||||
},
|
||||
{
|
||||
label: 'Off Value',
|
||||
id: 'offValue',
|
||||
placeholder: '1',
|
||||
tooltip: 'The value for "off values", may be a int, float, or null.',
|
||||
},
|
||||
];
|
||||
|
||||
export const PredictablePulseEditor = ({ onChange, query }: EditorProps) => {
|
||||
// Convert values to numbers before saving
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
onChange({ target: { name, value: Number(value) } });
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
{fields.map(({ label, id, placeholder, tooltip }) => {
|
||||
return (
|
||||
<InlineField label={label} labelWidth={14} key={id} tooltip={tooltip}>
|
||||
<Input
|
||||
width={32}
|
||||
type="number"
|
||||
name={id}
|
||||
id={`pulseWave.${id}`}
|
||||
value={query.pulseWave?.[id as keyof PulseWaveQuery]}
|
||||
placeholder={placeholder}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
})}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
42
public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx
vendored
Normal file
42
public/app/plugins/datasource/testdata/components/RandomWalkEditor.tsx
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { EditorProps } from '../QueryEditor';
|
||||
import { TestDataQuery } from '../types';
|
||||
|
||||
const randomWalkFields = [
|
||||
{ label: 'Series count', id: 'seriesCount', placeholder: '1', min: 1, step: 1 },
|
||||
{ label: 'Start value', id: 'startValue', placeholder: 'auto', step: 1 },
|
||||
{ label: 'Spread', id: 'spread', placeholder: '1', min: 0.5, step: 0.1 },
|
||||
{ label: 'Noise', id: 'noise', placeholder: '0', min: 0, step: 0.1 },
|
||||
{ label: 'Min', id: 'min', placeholder: 'none', step: 0.1 },
|
||||
{ label: 'Max', id: 'max', placeholder: 'none', step: 0.1 },
|
||||
];
|
||||
|
||||
const testSelectors = selectors.components.DataSource.TestData.QueryTab;
|
||||
type Selector = 'max' | 'min' | 'noise' | 'seriesCount' | 'spread' | 'startValue';
|
||||
|
||||
export const RandomWalkEditor = ({ onChange, query }: EditorProps) => {
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
{randomWalkFields.map(({ label, id, min, step, placeholder }) => {
|
||||
const selector = testSelectors?.[id as Selector];
|
||||
return (
|
||||
<InlineField label={label} labelWidth={14} key={id} aria-label={selector}>
|
||||
<Input
|
||||
width={32}
|
||||
name={id}
|
||||
type="number"
|
||||
id={id}
|
||||
min={min}
|
||||
step={step}
|
||||
value={query[id as keyof TestDataQuery] || placeholder}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
})}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
69
public/app/plugins/datasource/testdata/components/StreamingClientEditor.tsx
vendored
Normal file
69
public/app/plugins/datasource/testdata/components/StreamingClientEditor.tsx
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorProps } from '../QueryEditor';
|
||||
import { StreamingQuery } from '../types';
|
||||
|
||||
const streamingClientFields = [
|
||||
{ label: 'Speed (ms)', id: 'speed', placeholder: 'value', min: 10, step: 10 },
|
||||
{ label: 'Spread', id: 'spread', placeholder: 'value', min: 0.5, step: 0.1 },
|
||||
{ label: 'Noise', id: 'noise', placeholder: 'value', min: 0, step: 0.1 },
|
||||
{ label: 'Bands', id: 'bands', placeholder: 'bands', min: 0, step: 1 },
|
||||
];
|
||||
|
||||
const types = [
|
||||
{ value: 'signal', label: 'Signal' },
|
||||
{ value: 'logs', label: 'Logs' },
|
||||
{ value: 'fetch', label: 'Fetch' },
|
||||
];
|
||||
|
||||
export const StreamingClientEditor = ({ onChange, query }: EditorProps) => {
|
||||
const onSelectChange = ({ value }: SelectableValue) => {
|
||||
onChange({ target: { name: 'type', value } });
|
||||
};
|
||||
|
||||
// Convert values to numbers before saving
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
onChange({ target: { name, value: Number(value) } });
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Type" labelWidth={14}>
|
||||
<Select width={32} onChange={onSelectChange} defaultValue={types[0]} options={types} />
|
||||
</InlineField>
|
||||
{query?.stream?.type === 'signal' &&
|
||||
streamingClientFields.map(({ label, id, min, step, placeholder }) => {
|
||||
return (
|
||||
<InlineField label={label} labelWidth={14} key={id}>
|
||||
<Input
|
||||
width={32}
|
||||
type="number"
|
||||
id={`stream.${id}`}
|
||||
name={id}
|
||||
min={min}
|
||||
step={step}
|
||||
value={query.stream?.[id as keyof StreamingQuery]}
|
||||
placeholder={placeholder}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
})}
|
||||
|
||||
{query?.stream?.type === 'fetch' && (
|
||||
<InlineField label="URL" labelWidth={14} grow>
|
||||
<Input
|
||||
type="text"
|
||||
name="url"
|
||||
id="stream.url"
|
||||
value={query?.stream?.url}
|
||||
placeholder="Fetch URL"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
3
public/app/plugins/datasource/testdata/components/index.ts
vendored
Normal file
3
public/app/plugins/datasource/testdata/components/index.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export { StreamingClientEditor } from './StreamingClientEditor';
|
||||
export { ManualEntryEditor } from './ManualEntryEditor';
|
||||
export { RandomWalkEditor } from './RandomWalkEditor';
|
27
public/app/plugins/datasource/testdata/constants.ts
vendored
Normal file
27
public/app/plugins/datasource/testdata/constants.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
import { defaultQuery as defaultStreamQuery } from './runStreams';
|
||||
import { TestDataQuery } from './types';
|
||||
|
||||
export const defaultPulse: any = {
|
||||
timeStep: 60,
|
||||
onCount: 3,
|
||||
onValue: 2,
|
||||
offCount: 3,
|
||||
offValue: 1,
|
||||
};
|
||||
|
||||
export const defaultCSVWave: any = {
|
||||
timeStep: 60,
|
||||
valuesCSV: '0,0,2,2,1,1',
|
||||
};
|
||||
|
||||
export const defaultQuery: TestDataQuery = {
|
||||
points: [],
|
||||
stream: defaultStreamQuery,
|
||||
pulseWave: defaultPulse,
|
||||
csvWave: defaultCSVWave,
|
||||
stringInput: '',
|
||||
scenarioId: 'random_walk',
|
||||
lines: 10,
|
||||
refId: '',
|
||||
alias: '',
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { DataSourcePlugin } from '@grafana/data';
|
||||
import { TestDataDataSource } from './datasource';
|
||||
import { TestDataQueryCtrl } from './query_ctrl';
|
||||
import { TestInfoTab } from './TestInfoTab';
|
||||
import { ConfigEditor } from './ConfigEditor';
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
|
||||
class TestDataAnnotationsQueryCtrl {
|
||||
annotation: any;
|
||||
@ -12,7 +12,7 @@ class TestDataAnnotationsQueryCtrl {
|
||||
|
||||
export const plugin = new DataSourcePlugin(TestDataDataSource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setQueryCtrl(TestDataQueryCtrl)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setAnnotationQueryCtrl(TestDataAnnotationsQueryCtrl)
|
||||
.addConfigPage({
|
||||
title: 'Setup',
|
||||
|
@ -1,324 +0,0 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Scenario</label>
|
||||
<div class="gf-form-select-wrapper width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()" aria-label={{ctrl.selectors.scenarioSelect}}></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form gf-form--grow" ng-if="ctrl.scenario.stringInput">
|
||||
<label class="gf-form-label query-keyword">String Input</label>
|
||||
<input type="text" class="gf-form-input" placeholder="{{ctrl.scenario.stringInput}}" ng-model="ctrl.target.stringInput" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Alias</label>
|
||||
<input type="text" class="gf-form-input width-14" placeholder="optional" ng-model="ctrl.target.alias" ng-model-onblur ng-change="ctrl.refresh()" pattern='[^<>&\\"]+'>
|
||||
</div>
|
||||
<div ng-if="ctrl.showLabels" class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
Labels
|
||||
<info-popover mode="right-normal">
|
||||
Set labels using a key=value syntax:<br/>
|
||||
{key="value", key2="value"}<br/>
|
||||
key="value", key2="value"<br/>
|
||||
key=value, key2=value<br/>
|
||||
</info-popover>
|
||||
</label>
|
||||
<input type="text" class="gf-form-input gf-form--grow" placeholder='key=value, key2=value2' ng-model="ctrl.target.labels" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
</div>
|
||||
<div ng-if="!ctrl.showLabels" class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'manual_entry'">
|
||||
<div class="gf-form gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">New value</label>
|
||||
<input type="number" class="gf-form-input width-15" placeholder="value" ng-model="ctrl.newPointValue">
|
||||
<label class="gf-form-label query-keyword">Time</label>
|
||||
<input type="string" class="gf-form-input width-12" placeholder="time" ng-model="ctrl.newPointTime">
|
||||
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addPoint()">Add</button>
|
||||
<label class="gf-form-label query-keyword">All values</label>
|
||||
<gf-form-dropdown css-class="width-12" model="ctrl.selectedPoint" get-options="ctrl.getPoints()" on-change="ctrl.pointSelected($option)">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
<div class="gf-form gf-form" ng-if="ctrl.selectedPoint.value !== null">
|
||||
<button class="btn btn-danger gf-form-btn" ng-click="ctrl.deletePoint()">Delete</button>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'random_walk'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Series count</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-15"
|
||||
placeholder="1"
|
||||
ng-model="ctrl.target.seriesCount"
|
||||
min="1"
|
||||
step="1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.seriesCount}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Start value</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-14"
|
||||
placeholder="auto"
|
||||
ng-model="ctrl.target.startValue"
|
||||
step="1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.startValue}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Spread</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-6"
|
||||
placeholder="1"
|
||||
ng-model="ctrl.target.spread"
|
||||
min="0.5"
|
||||
step="0.1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.spread}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Noise</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-6"
|
||||
placeholder="0"
|
||||
ng-model="ctrl.target.noise"
|
||||
min="0"
|
||||
step="0.1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.noise}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Min</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-15"
|
||||
placeholder="none"
|
||||
ng-model="ctrl.target.min"
|
||||
step="0.1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.min}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Max</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-15"
|
||||
placeholder="none"
|
||||
ng-model="ctrl.target.max"
|
||||
step="0.1"
|
||||
ng-change="ctrl.refresh()"
|
||||
aria-label="{{::ctrl.selectors.max}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'streaming_client'">
|
||||
<div class="gf-form gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Type</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select
|
||||
ng-model="ctrl.target.stream.type"
|
||||
class="gf-form-input"
|
||||
ng-options="type for type in ['signal','logs', 'fetch']"
|
||||
ng-change="ctrl.streamChanged()" ></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword">Speed (ms)</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="value"
|
||||
ng-model="ctrl.target.stream.speed"
|
||||
min="10"
|
||||
step="10"
|
||||
ng-change="ctrl.streamChanged()"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.target.stream.type === 'signal'">
|
||||
<label class="gf-form-label query-keyword">Spread</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="value"
|
||||
ng-model="ctrl.target.stream.spread"
|
||||
min="0.5"
|
||||
step="0.1"
|
||||
ng-change="ctrl.streamChanged()" />
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.target.stream.type === 'signal'">
|
||||
<label class="gf-form-label query-keyword">Noise</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="value"
|
||||
ng-model="ctrl.target.stream.noise"
|
||||
min="0"
|
||||
step="0.1"
|
||||
ng-change="ctrl.streamChanged()" />
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.target.stream.type === 'signal'">
|
||||
<label class="gf-form-label query-keyword">Bands</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="bands"
|
||||
ng-model="ctrl.target.stream.bands"
|
||||
min="0"
|
||||
step="1"
|
||||
ng-change="ctrl.streamChanged()" />
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="ctrl.target.stream.type === 'fetch'">
|
||||
<label class="gf-form-label query-keyword">URL</label>
|
||||
<input type="string"
|
||||
class="gf-form-input gf-form-label--grow"
|
||||
placeholder="Fetch URL"
|
||||
ng-model="ctrl.target.stream.url"
|
||||
ng-change="ctrl.streamChanged()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'logs'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword">Lines</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="10"
|
||||
ng-model="ctrl.target.lines"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch class="gf-form" label="Level" label-class="query-keyword width-5" checked="ctrl.target.levelColumn" switch-class="max-width-6" on-change="ctrl.refresh()"></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'grafana_api'">
|
||||
<div class="gf-form gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">Endpoint</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select
|
||||
ng-model="ctrl.target.stringInput"
|
||||
class="gf-form-input"
|
||||
ng-options="type for type in ['datasources', 'search', 'annotations']"
|
||||
ng-change="ctrl.refresh()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'arrow'">
|
||||
<div class="gf-form" style="width: 100%;">
|
||||
<textarea type="string"
|
||||
class="gf-form-input"
|
||||
rows="10"
|
||||
placeholder="copy base64 text data from query result"
|
||||
ng-model="ctrl.target.stringInput"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur ></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Predictable Pulse Scenario Options Form -->
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'predictable_pulse'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
Step
|
||||
<info-popover mode="right-normal">The number of seconds between datapoints.</info-popover>
|
||||
</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="60"
|
||||
ng-model="ctrl.target.pulseWave.timeStep"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
On Count
|
||||
<info-popover mode="right-normal">The number of values within a cycle, at the start of the cycle, that should have the onValue.</info-popover>
|
||||
</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-3"
|
||||
placeholder="3"
|
||||
ng-model="ctrl.target.pulseWave.onCount"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
Off Count
|
||||
<info-popover mode="right-normal">The number of offValues within the cycle.</info-popover>
|
||||
</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-3"
|
||||
placeholder="6"
|
||||
ng-model="ctrl.target.pulseWave.offCount"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
On Value
|
||||
<info-popover mode="right-normal">The value for "on values", may be a int, float, or null.</info-popover>
|
||||
</label>
|
||||
<input type="string"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="1"
|
||||
ng-model="ctrl.target.pulseWave.onValue"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
Off Value
|
||||
<info-popover mode="right-normal">The value for "off values", may be a int, float, or null.</info-popover>
|
||||
</label>
|
||||
<input type="string"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="1"
|
||||
ng-model="ctrl.target.pulseWave.offValue"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Predictable CSV Wave Scenario Options Form -->
|
||||
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'predictable_csv_wave'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-7">
|
||||
Step
|
||||
<info-popover mode="right-normal">The number of seconds between datapoints.</info-popover>
|
||||
</label>
|
||||
<input type="number"
|
||||
class="gf-form-input width-5"
|
||||
placeholder="60"
|
||||
ng-model="ctrl.target.csvWave.timeStep"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label query-keyword width-10">
|
||||
CSV Values
|
||||
<info-popover mode="right-normal">Comma separated values. Each value may be an int, float, or null and must not be empty. Whitespace and trailing commas are removed.</info-popover>
|
||||
</label>
|
||||
<input type="string"
|
||||
class="gf-form-input gf-form-label--grow"
|
||||
placeholder="1,2,3,2"
|
||||
ng-model="ctrl.target.csvWave.valuesCSV"
|
||||
ng-change="ctrl.refresh()"
|
||||
ng-model-onblur />
|
||||
</div>
|
||||
</div>
|
||||
</query-editor-row>
|
131
public/app/plugins/datasource/testdata/query_ctrl.ts
vendored
131
public/app/plugins/datasource/testdata/query_ctrl.ts
vendored
@ -1,131 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { IScope } from 'angular';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { dateMath, dateTime } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { defaultQuery } from './runStreams';
|
||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||
|
||||
export const defaultPulse: any = {
|
||||
timeStep: 60,
|
||||
onCount: 3,
|
||||
onValue: 2,
|
||||
offCount: 3,
|
||||
offValue: 1,
|
||||
};
|
||||
|
||||
export const defaultCSVWave: any = {
|
||||
timeStep: 60,
|
||||
valuesCSV: '0,0,2,2,1,1',
|
||||
};
|
||||
|
||||
const showLabelsFor = ['random_walk', 'predictable_pulse', 'predictable_csv_wave'];
|
||||
|
||||
export class TestDataQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
scenarioList: any;
|
||||
scenario: any;
|
||||
newPointValue: number;
|
||||
newPointTime: any;
|
||||
selectedPoint: any;
|
||||
digest: (promise: Promise<any>) => Promise<any>;
|
||||
|
||||
showLabels = false;
|
||||
selectors: typeof selectors.components.DataSource.TestData.QueryTab;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: IScope, $injector: any) {
|
||||
super($scope, $injector);
|
||||
|
||||
this.target.scenarioId = this.target.scenarioId || 'random_walk';
|
||||
this.scenarioList = [];
|
||||
this.newPointTime = dateTime();
|
||||
this.selectedPoint = { text: 'Select point', value: null };
|
||||
this.showLabels = showLabelsFor.includes(this.target.scenarioId);
|
||||
this.selectors = selectors.components.DataSource.TestData.QueryTab;
|
||||
}
|
||||
|
||||
getPoints() {
|
||||
return _.map(this.target.points, (point, index) => {
|
||||
return {
|
||||
text: dateTime(point[1]).format('MMMM Do YYYY, H:mm:ss') + ' : ' + point[0],
|
||||
value: index,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pointSelected(option: any) {
|
||||
this.selectedPoint = option;
|
||||
}
|
||||
|
||||
deletePoint() {
|
||||
this.target.points.splice(this.selectedPoint.value, 1);
|
||||
this.selectedPoint = { text: 'Select point', value: null };
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
addPoint() {
|
||||
this.target.points = this.target.points || [];
|
||||
this.newPointTime = dateMath.parse(this.newPointTime);
|
||||
this.target.points.push([this.newPointValue, this.newPointTime.valueOf()]);
|
||||
this.target.points = _.sortBy(this.target.points, p => p[1]);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return promiseToDigest(this.$scope)(
|
||||
getBackendSrv()
|
||||
.get('/api/tsdb/testdata/scenarios')
|
||||
.then((res: any) => {
|
||||
this.scenarioList = res;
|
||||
this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
scenarioChanged() {
|
||||
this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId });
|
||||
|
||||
if (this.target.scenarioId === 'manual_entry') {
|
||||
this.target.points = this.target.points || [];
|
||||
} else {
|
||||
delete this.target.points;
|
||||
}
|
||||
|
||||
if (this.target.scenarioId === 'streaming_client') {
|
||||
this.target.stream = _.defaults(this.target.stream || {}, defaultQuery);
|
||||
} else {
|
||||
delete this.target.stream;
|
||||
}
|
||||
|
||||
if (this.target.scenarioId === 'predictable_pulse') {
|
||||
this.target.pulseWave = _.defaults(this.target.pulseWave || {}, defaultPulse);
|
||||
} else {
|
||||
delete this.target.pulseWave;
|
||||
}
|
||||
|
||||
if (this.target.scenarioId === 'predictable_csv_wave') {
|
||||
this.target.csvWave = _.defaults(this.target.csvWave || {}, defaultCSVWave);
|
||||
} else {
|
||||
delete this.target.csvWave;
|
||||
}
|
||||
|
||||
if (this.target.scenarioId === 'grafana_api') {
|
||||
this.target.stringInput = 'datasources';
|
||||
} else {
|
||||
delete this.target.stringInput;
|
||||
}
|
||||
|
||||
this.target.stringInput = this.scenario.stringInput ?? undefined;
|
||||
this.showLabels = showLabelsFor.includes(this.target.scenarioId);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
streamChanged() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
24
public/app/plugins/datasource/testdata/types.ts
vendored
24
public/app/plugins/datasource/testdata/types.ts
vendored
@ -3,14 +3,28 @@ import { DataQuery } from '@grafana/data';
|
||||
export interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
stringInput: string;
|
||||
}
|
||||
|
||||
export type PointValue = number;
|
||||
|
||||
export interface NewPoint {
|
||||
newPointValue: string;
|
||||
newPointTime: string;
|
||||
}
|
||||
export type Points = PointValue[][];
|
||||
|
||||
export interface TestDataQuery extends DataQuery {
|
||||
alias?: string;
|
||||
scenarioId: string;
|
||||
stringInput: string;
|
||||
points?: any[];
|
||||
points: Points;
|
||||
stream?: StreamingQuery;
|
||||
pulseWave?: PulseWaveQuery;
|
||||
csvWave: any;
|
||||
labels?: string;
|
||||
lines?: number;
|
||||
levelColumn?: boolean;
|
||||
}
|
||||
|
||||
export interface StreamingQuery {
|
||||
@ -21,3 +35,11 @@ export interface StreamingQuery {
|
||||
bands?: number; // number of bands around the middle band
|
||||
url?: string; // the Fetch URL
|
||||
}
|
||||
|
||||
export interface PulseWaveQuery {
|
||||
timeStep?: number;
|
||||
onCount?: number;
|
||||
offCount?: number;
|
||||
onValue?: number;
|
||||
offValue?: number;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user