From e3a648e107f069e768f572d38458b4a419a7606f Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:53:09 +0100 Subject: [PATCH] Dashboard: Migration - EditVariable Settings: Implement Interval Variable (#81259) * Extract logic from core IntervalEditor and create a new Form to be shared between scenes and core * Implement IntervalVariableEditor and refactor some utils functions * Add unit test --- .../src/selectors/pages.ts | 3 + .../transformSaveModelToScene.ts | 4 +- .../components/IntervalVariableForm.tsx | 96 +++++++++++++++ .../editors/IntervalVariableEditor.test.tsx | 115 ++++++++++++++++++ .../editors/IntervalVariableEditor.tsx | 49 +++++++- .../settings/variables/utils.ts | 3 +- .../features/dashboard-scene/utils/utils.ts | 6 +- .../interval/IntervalVariableEditor.tsx | 86 ++----------- 8 files changed, 278 insertions(+), 84 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx create mode 100644 public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index eac5b447914..285b0447aa2 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -173,6 +173,9 @@ export const Pages = { }, IntervalVariable: { intervalsValueInput: 'data-testid interval variable intervals input', + autoEnabledCheckbox: 'data-testid interval variable auto value checkbox', + stepCountIntervalSelect: 'data-testid interval variable step count input', + minIntervalInput: 'data-testid interval variable mininum interval input', }, }, }, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 3f0f3146cd0..07e4b2c410c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -50,7 +50,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider'; import { DashboardInteractions } from '../utils/interactions'; import { getCurrentValueForOldIntervalModel, - getIntervalsFromOldIntervalModel, + getIntervalsFromQueryString, getVizPanelKeyForPanelId, } from '../utils/utils'; @@ -350,7 +350,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode hide: variable.hide, }); } else if (variable.type === 'interval') { - const intervals = getIntervalsFromOldIntervalModel(variable); + const intervals = getIntervalsFromQueryString(variable.query); const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); return new IntervalVariable({ ...commonProperties, diff --git a/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx new file mode 100644 index 00000000000..3f525e2d8fe --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx @@ -0,0 +1,96 @@ +import { css } from '@emotion/css'; +import React, { ChangeEvent, FormEvent } from 'react'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { useStyles2 } from '@grafana/ui'; + +import { VariableCheckboxField } from './VariableCheckboxField'; +import { VariableLegend } from './VariableLegend'; +import { VariableSelectField } from './VariableSelectField'; +import { VariableTextField } from './VariableTextField'; + +interface IntervalVariableFormProps { + intervals: string; + onIntervalsChange: (event: FormEvent) => void; + onAutoEnabledChange: (event: ChangeEvent) => void; + onAutoMinIntervalChanged: (event: FormEvent) => void; + onAutoCountChanged: (option: SelectableValue) => void; + autoEnabled: boolean; + autoMinInterval: string; + autoStepCount: number; +} + +export function IntervalVariableForm({ + intervals, + onIntervalsChange, + onAutoEnabledChange, + onAutoMinIntervalChanged, + onAutoCountChanged, + autoEnabled, + autoMinInterval, + autoStepCount, +}: IntervalVariableFormProps) { + const STEP_OPTIONS = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map((count) => ({ + label: `${count}`, + value: count, + })); + const styles = useStyles2(getStyles); + + const stepCount = STEP_OPTIONS.find((option) => option.value === autoStepCount) ?? STEP_OPTIONS[0]; + + return ( + <> + Interval options + + + + {autoEnabled && ( +
+ + +
+ )} + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + autoFields: css({ + marginTop: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }), + }; +}; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx new file mode 100644 index 00000000000..9e2e16c44ec --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx @@ -0,0 +1,115 @@ +// unit test for IntervalVariableEditor component + +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { IntervalVariable } from '@grafana/scenes'; + +import { IntervalVariableEditor } from './IntervalVariableEditor'; + +describe('IntervalVariableEditor', () => { + it('should render correctly', () => { + const variable = new IntervalVariable({ + name: 'test', + type: 'interval', + intervals: ['1m', '10m', '1h', '6h', '1d', '7d'], + }); + + const onRunQuery = jest.fn(); + + const { getByTestId, queryByTestId } = render( + + ); + const intervalsInput = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput + ); + const autoEnabledCheckbox = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.autoEnabledCheckbox + ); + + expect(intervalsInput).toBeInTheDocument(); + expect(intervalsInput).toHaveValue('1m,10m,1h,6h,1d,7d'); + expect(autoEnabledCheckbox).toBeInTheDocument(); + expect(autoEnabledCheckbox).not.toBeChecked(); + expect( + queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.minIntervalInput) + ).toBeNull(); + expect( + queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.stepCountIntervalSelect) + ).toBeNull(); + }); + + it('should update intervals correctly', async () => { + const variable = new IntervalVariable({ + name: 'test', + type: 'interval', + intervals: ['1m', '10m', '1h', '6h', '1d', '7d'], + }); + + const onRunQuery = jest.fn(); + + const { user, getByTestId } = setup(); + const intervalsInput = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput + ); + + await user.clear(intervalsInput); + await user.type(intervalsInput, '7d,30d, 1y, 5y, 10y'); + await user.tab(); + + expect(intervalsInput).toBeInTheDocument(); + expect(intervalsInput).toHaveValue('7d,30d, 1y, 5y, 10y'); + expect(onRunQuery).toHaveBeenCalledTimes(1); + }); + + it('should handle auto enabled option correctly', async () => { + const variable = new IntervalVariable({ + name: 'test', + type: 'interval', + intervals: ['1m', '10m', '1h', '6h', '1d', '7d'], + autoEnabled: false, + }); + + const onRunQuery = jest.fn(); + + const { user, getByTestId, queryByTestId } = setup( + + ); + + const autoEnabledCheckbox = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.autoEnabledCheckbox + ); + + await user.click(autoEnabledCheckbox); + + const minIntervalInput = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.minIntervalInput + ); + + const stepCountIntervalSelect = queryByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.stepCountIntervalSelect + ); + + await waitFor(() => { + expect(autoEnabledCheckbox).toBeInTheDocument(); + expect(autoEnabledCheckbox).toBeChecked(); + expect(minIntervalInput).toBeInTheDocument(); + expect(stepCountIntervalSelect).toBeInTheDocument(); + expect(minIntervalInput).toHaveValue('10s'); + }); + + await user.clear(minIntervalInput); + await user.type(minIntervalInput, '10m'); + await user.tab(); + expect(minIntervalInput).toHaveValue('10m'); + }); +}); + +function setup(jsx: JSX.Element) { + return { + user: userEvent.setup(), + ...render(jsx), + }; +} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx index 524ef32c41a..6ff69c2b0c5 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx @@ -1,12 +1,53 @@ -import React from 'react'; +import React, { ChangeEvent, FormEvent } from 'react'; +import { SelectableValue } from '@grafana/data'; import { IntervalVariable } from '@grafana/scenes'; +import { + getIntervalsFromQueryString, + getIntervalsQueryFromNewIntervalModel, +} from 'app/features/dashboard-scene/utils/utils'; + +import { IntervalVariableForm } from '../components/IntervalVariableForm'; interface IntervalVariableEditorProps { variable: IntervalVariable; - onChange: (variable: IntervalVariable) => void; + onRunQuery: () => void; } -export function IntervalVariableEditor(props: IntervalVariableEditorProps) { - return
IntervalVariableEditor
; +export function IntervalVariableEditor({ variable, onRunQuery }: IntervalVariableEditorProps) { + const { intervals, autoStepCount, autoEnabled, autoMinInterval } = variable.useState(); + + //transform intervals array into string + const intervalsCombined = getIntervalsQueryFromNewIntervalModel(intervals); + + const onIntervalsChange = (event: FormEvent) => { + const intervalsArray = getIntervalsFromQueryString(event.currentTarget.value); + variable.setState({ intervals: intervalsArray }); + onRunQuery(); + }; + + const onAutoCountChanged = (option: SelectableValue) => { + variable.setState({ autoStepCount: option.value }); + }; + + const onAutoEnabledChange = (event: ChangeEvent) => { + variable.setState({ autoEnabled: event.target.checked }); + }; + + const onAutoMinIntervalChanged = (event: FormEvent) => { + variable.setState({ autoMinInterval: event.currentTarget.value }); + }; + + return ( + + ); } diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index 1a3076fbd75..4b1514a336e 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -123,7 +123,8 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo } export function hasVariableOptions(variable: SceneVariable): variable is MultiValueVariable { - return 'options' in variable.state; + // variable options can be defined by state.options or state.intervals in case of interval variable + return 'options' in variable.state || 'intervals' in variable.state; } export function getDefinition(model: SceneVariable): string { diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index a73a56e8e14..5e1f3074693 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -94,10 +94,10 @@ export function getMultiVariableValues(variable: MultiValueVariable) { }; } -// Transform old interval model to new interval model from scenes -export function getIntervalsFromOldIntervalModel(variable: IntervalVariableModel): string[] { +// used to transform old interval model to new interval model from scenes +export function getIntervalsFromQueryString(query: string): string[] { // separate intervals by quotes either single or double - const matchIntervals = variable.query.match(/(["'])(.*?)\1|\w+/g); + const matchIntervals = query.match(/(["'])(.*?)\1|\w+/g); // If no intervals are found in query, return the initial state of the interval reducer. if (!matchIntervals) { diff --git a/public/app/features/variables/interval/IntervalVariableEditor.tsx b/public/app/features/variables/interval/IntervalVariableEditor.tsx index d9e46c29462..88504924769 100644 --- a/public/app/features/variables/interval/IntervalVariableEditor.tsx +++ b/public/app/features/variables/interval/IntervalVariableEditor.tsx @@ -1,21 +1,10 @@ -import { css } from '@emotion/css'; import React, { ChangeEvent, FormEvent } from 'react'; -import { GrafanaTheme2, IntervalVariableModel, SelectableValue } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { useStyles2 } from '@grafana/ui'; +import { IntervalVariableModel, SelectableValue } from '@grafana/data'; +import { IntervalVariableForm } from 'app/features/dashboard-scene/settings/variables/components/IntervalVariableForm'; -import { VariableCheckboxField } from '../../dashboard-scene/settings/variables/components/VariableCheckboxField'; -import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend'; -import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField'; -import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField'; import { VariableEditorProps } from '../editor/types'; -const STEP_OPTIONS = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map((count) => ({ - label: `${count}`, - value: count, -})); - export interface Props extends VariableEditorProps {} export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Props) => { @@ -27,13 +16,6 @@ export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Pr }); }; - const onQueryChanged = (event: FormEvent) => { - onPropChange({ - propName: 'query', - propValue: event.currentTarget.value, - }); - }; - const onQueryBlur = (event: FormEvent) => { onPropChange({ propName: 'query', @@ -58,62 +40,18 @@ export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Pr }); }; - const stepValue = STEP_OPTIONS.find((o) => o.value === variable.auto_count) ?? STEP_OPTIONS[0]; - - const styles = useStyles2(getStyles); - return ( - <> - Interval options - - - - {variable.auto && ( -
- - -
- )} - + ); }); IntervalVariableEditor.displayName = 'IntervalVariableEditor'; - -function getStyles(theme: GrafanaTheme2) { - return { - autoFields: css({ - marginTop: theme.spacing(2), - display: 'flex', - flexDirection: 'column', - }), - }; -}