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
This commit is contained in:
Alexa V 2024-01-29 16:53:09 +01:00 committed by GitHub
parent da1538ba82
commit e3a648e107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 278 additions and 84 deletions

View File

@ -173,6 +173,9 @@ export const Pages = {
}, },
IntervalVariable: { IntervalVariable: {
intervalsValueInput: 'data-testid interval variable intervals input', 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',
}, },
}, },
}, },

View File

@ -50,7 +50,7 @@ import { createPanelDataProvider } from '../utils/createPanelDataProvider';
import { DashboardInteractions } from '../utils/interactions'; import { DashboardInteractions } from '../utils/interactions';
import { import {
getCurrentValueForOldIntervalModel, getCurrentValueForOldIntervalModel,
getIntervalsFromOldIntervalModel, getIntervalsFromQueryString,
getVizPanelKeyForPanelId, getVizPanelKeyForPanelId,
} from '../utils/utils'; } from '../utils/utils';
@ -350,7 +350,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
hide: variable.hide, hide: variable.hide,
}); });
} else if (variable.type === 'interval') { } else if (variable.type === 'interval') {
const intervals = getIntervalsFromOldIntervalModel(variable); const intervals = getIntervalsFromQueryString(variable.query);
const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals); const currentInterval = getCurrentValueForOldIntervalModel(variable, intervals);
return new IntervalVariable({ return new IntervalVariable({
...commonProperties, ...commonProperties,

View File

@ -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<HTMLInputElement>) => void;
onAutoEnabledChange: (event: ChangeEvent<HTMLInputElement>) => void;
onAutoMinIntervalChanged: (event: FormEvent<HTMLInputElement>) => 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 (
<>
<VariableLegend>Interval options</VariableLegend>
<VariableTextField
defaultValue={intervals}
name="Values"
placeholder="1m,10m,1h,6h,1d,7d"
onBlur={onIntervalsChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput}
width={32}
required
/>
<VariableCheckboxField
value={autoEnabled}
name="Auto option"
description="Dynamically calculates interval by dividing time range by the count specified"
onChange={onAutoEnabledChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.autoEnabledCheckbox}
/>
{autoEnabled && (
<div className={styles.autoFields}>
<VariableSelectField
name="Step count"
description="How many times the current time range should be divided to calculate the value"
value={stepCount}
options={STEP_OPTIONS}
onChange={onAutoCountChanged}
width={9}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.stepCountIntervalSelect}
/>
<VariableTextField
value={autoMinInterval}
name="Min interval"
description="The calculated value will not go below this threshold"
placeholder="10s"
onChange={onAutoMinIntervalChanged}
width={11}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.minIntervalInput}
/>
</div>
)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
autoFields: css({
marginTop: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
}),
};
};

View File

@ -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(
<IntervalVariableEditor variable={variable} onRunQuery={onRunQuery} />
);
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(<IntervalVariableEditor variable={variable} onRunQuery={onRunQuery} />);
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(
<IntervalVariableEditor variable={variable} onRunQuery={onRunQuery} />
);
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),
};
}

View File

@ -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 { IntervalVariable } from '@grafana/scenes';
import {
getIntervalsFromQueryString,
getIntervalsQueryFromNewIntervalModel,
} from 'app/features/dashboard-scene/utils/utils';
import { IntervalVariableForm } from '../components/IntervalVariableForm';
interface IntervalVariableEditorProps { interface IntervalVariableEditorProps {
variable: IntervalVariable; variable: IntervalVariable;
onChange: (variable: IntervalVariable) => void; onRunQuery: () => void;
} }
export function IntervalVariableEditor(props: IntervalVariableEditorProps) { export function IntervalVariableEditor({ variable, onRunQuery }: IntervalVariableEditorProps) {
return <div>IntervalVariableEditor</div>; const { intervals, autoStepCount, autoEnabled, autoMinInterval } = variable.useState();
//transform intervals array into string
const intervalsCombined = getIntervalsQueryFromNewIntervalModel(intervals);
const onIntervalsChange = (event: FormEvent<HTMLInputElement>) => {
const intervalsArray = getIntervalsFromQueryString(event.currentTarget.value);
variable.setState({ intervals: intervalsArray });
onRunQuery();
};
const onAutoCountChanged = (option: SelectableValue<number>) => {
variable.setState({ autoStepCount: option.value });
};
const onAutoEnabledChange = (event: ChangeEvent<HTMLInputElement>) => {
variable.setState({ autoEnabled: event.target.checked });
};
const onAutoMinIntervalChanged = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ autoMinInterval: event.currentTarget.value });
};
return (
<IntervalVariableForm
intervals={intervalsCombined}
autoStepCount={autoStepCount}
autoEnabled={autoEnabled}
onAutoCountChanged={onAutoCountChanged}
onIntervalsChange={onIntervalsChange}
onAutoEnabledChange={onAutoEnabledChange}
onAutoMinIntervalChanged={onAutoMinIntervalChanged}
autoMinInterval={autoMinInterval}
/>
);
} }

View File

@ -123,7 +123,8 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo
} }
export function hasVariableOptions(variable: SceneVariable): variable is MultiValueVariable { 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 { export function getDefinition(model: SceneVariable): string {

View File

@ -94,10 +94,10 @@ export function getMultiVariableValues(variable: MultiValueVariable) {
}; };
} }
// Transform old interval model to new interval model from scenes // used to transform old interval model to new interval model from scenes
export function getIntervalsFromOldIntervalModel(variable: IntervalVariableModel): string[] { export function getIntervalsFromQueryString(query: string): string[] {
// separate intervals by quotes either single or double // 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 no intervals are found in query, return the initial state of the interval reducer.
if (!matchIntervals) { if (!matchIntervals) {

View File

@ -1,21 +1,10 @@
import { css } from '@emotion/css';
import React, { ChangeEvent, FormEvent } from 'react'; import React, { ChangeEvent, FormEvent } from 'react';
import { GrafanaTheme2, IntervalVariableModel, SelectableValue } from '@grafana/data'; import { IntervalVariableModel, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { IntervalVariableForm } from 'app/features/dashboard-scene/settings/variables/components/IntervalVariableForm';
import { useStyles2 } from '@grafana/ui';
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'; 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<IntervalVariableModel> {} export interface Props extends VariableEditorProps<IntervalVariableModel> {}
export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Props) => { export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Props) => {
@ -27,13 +16,6 @@ export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Pr
}); });
}; };
const onQueryChanged = (event: FormEvent<HTMLInputElement>) => {
onPropChange({
propName: 'query',
propValue: event.currentTarget.value,
});
};
const onQueryBlur = (event: FormEvent<HTMLInputElement>) => { const onQueryBlur = (event: FormEvent<HTMLInputElement>) => {
onPropChange({ onPropChange({
propName: 'query', 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 ( return (
<> <IntervalVariableForm
<VariableLegend>Interval options</VariableLegend> intervals={variable.query}
<VariableTextField autoStepCount={variable.auto_count}
value={variable.query} autoEnabled={variable.auto}
name="Values" onAutoCountChanged={onAutoCountChanged}
placeholder="1m,10m,1h,6h,1d,7d" onIntervalsChange={onQueryBlur}
onChange={onQueryChanged} onAutoEnabledChange={onAutoChange}
onBlur={onQueryBlur} onAutoMinIntervalChanged={onAutoMinChanged}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput} autoMinInterval={variable.auto_min}
width={32}
required
/> />
<VariableCheckboxField
value={variable.auto}
name="Auto option"
description="Dynamically calculates interval by dividing time range by the count specified"
onChange={onAutoChange}
/>
{variable.auto && (
<div className={styles.autoFields}>
<VariableSelectField
name="Step count"
description="How many times the current time range should be divided to calculate the value"
value={stepValue}
options={STEP_OPTIONS}
onChange={onAutoCountChanged}
width={9}
/>
<VariableTextField
value={variable.auto_min}
name="Min interval"
description="The calculated value will not go below this threshold"
placeholder="10s"
onChange={onAutoMinChanged}
width={11}
/>
</div>
)}
</>
); );
}); });
IntervalVariableEditor.displayName = 'IntervalVariableEditor'; IntervalVariableEditor.displayName = 'IntervalVariableEditor';
function getStyles(theme: GrafanaTheme2) {
return {
autoFields: css({
marginTop: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
}),
};
}