diff --git a/.prettierignore b/.prettierignore index 336d03e2551..eb6e109a005 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,4 +6,4 @@ node_modules public/vendor/ vendor/ data/ - +e2e/tmp diff --git a/e2e/shared/smokeTestScenario.ts b/e2e/shared/smokeTestScenario.ts index fdfbab68fa1..adaf2dde076 100644 --- a/e2e/shared/smokeTestScenario.ts +++ b/e2e/shared/smokeTestScenario.ts @@ -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'); diff --git a/e2e/suite1/specs/panelEdit_queries.spec.ts b/e2e/suite1/specs/panelEdit_queries.spec.ts index 486e486a232..43d77d9ba41 100644 --- a/e2e/suite1/specs/panelEdit_queries.spec.ts +++ b/e2e/suite1/specs/panelEdit_queries.spec.ts @@ -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'); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 61fa2922ecf..2e6973ac4b8 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -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', diff --git a/public/app/plugins/datasource/testdata/QueryEditor.test.tsx b/public/app/plugins/datasource/testdata/QueryEditor.test.tsx new file mode 100644 index 00000000000..84e2dd4077c --- /dev/null +++ b/public/app/plugins/datasource/testdata/QueryEditor.test.tsx @@ -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) => { + const editorProps = { ...props, ...testProps }; + return render(); +}; + +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( + + ); + 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( + + ); + 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(); + 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); + }); +}); diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index 822a39c749b..f3cb7592522 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -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; +export type Props = QueryEditorProps; -export class QueryEditor extends PureComponent { - backendSrv = getBackendSrv(); +export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) => { + query = { ...defaultQuery, ...query }; - state: State = { - scenarioList: [], - current: null, + const { loading, value: scenarioList } = useAsync(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) => { + const scenario = scenarioList?.find(sc => sc.id === item.value); - this.setState({ scenarioList: scenarioList, current: current }); - } + if (!scenario) { + return; + } - onScenarioChange = (item: SelectableValue) => { - 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) => { + const { name, value } = e.target as HTMLInputElement | HTMLTextAreaElement; + let newValue: Partial = { [name]: value }; - return ( -
-
- - Scenario - - item.value === query.scenarioId)} + onChange={onScenarioChange} + width={32} + /> + + {currentScenario?.stringInput && ( + + + + )} + + + + {showLabels && ( + + Set labels using a key=value syntax: +
+ {`{ key = "value", key2 = "value" }`} +
+ key="value", key2="value" +
+ key=value, key2=value +
+ + } + > + +
+ )} + + + {scenarioId === 'manual_entry' && } + {scenarioId === 'random_walk' && } + {scenarioId === 'streaming_client' && } + {scenarioId === 'logs' && ( + + + + + + + + + )} + + {scenarioId === 'grafana_api' && ( + + -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- diff --git a/public/app/plugins/datasource/testdata/query_ctrl.ts b/public/app/plugins/datasource/testdata/query_ctrl.ts deleted file mode 100644 index 2a2a6ea2467..00000000000 --- a/public/app/plugins/datasource/testdata/query_ctrl.ts +++ /dev/null @@ -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) => Promise; - - 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(); - } -} diff --git a/public/app/plugins/datasource/testdata/types.ts b/public/app/plugins/datasource/testdata/types.ts index 27882719746..997b4ee4103 100644 --- a/public/app/plugins/datasource/testdata/types.ts +++ b/public/app/plugins/datasource/testdata/types.ts @@ -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; +}