Dashboard group by variable edit pane (#106104)

* Dashboards: Group By Variable in edit pane
pull/106176/head
Scott Lepper 2 months ago committed by GitHub
parent 80c47a64b1
commit 1de0cd5d68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 24
      public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.test.tsx
  3. 132
      public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx
  4. 37
      public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.test.tsx
  5. 25
      public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.tsx
  6. 3
      public/app/features/dashboard-scene/settings/variables/utils.ts
  7. 1
      public/locales/en-US/grafana.json

@ -1749,8 +1749,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
], ],
"public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx:5381": [ "public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
], ],
"public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx:5381": [ "public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]

@ -45,6 +45,7 @@ describe('GroupByVariableForm', () => {
onAllowCustomValueChange: onAllowCustomValueChangeMock, onAllowCustomValueChange: onAllowCustomValueChangeMock,
onDataSourceChange: onDataSourceChangeMock, onDataSourceChange: onDataSourceChangeMock,
onDefaultOptionsChange: onDefaultOptionsChangeMock, onDefaultOptionsChange: onDefaultOptionsChangeMock,
datasourceSupported: true,
}; };
function setup(props?: Partial<GroupByVariableFormProps>) { function setup(props?: Partial<GroupByVariableFormProps>) {
@ -130,4 +131,27 @@ describe('GroupByVariableForm', () => {
expect(onDefaultOptionsChangeMock).toHaveBeenCalledTimes(1); expect(onDefaultOptionsChangeMock).toHaveBeenCalledTimes(1);
expect(onDefaultOptionsChangeMock).toHaveBeenCalledWith(undefined); expect(onDefaultOptionsChangeMock).toHaveBeenCalledWith(undefined);
}); });
it('should render only datasource picker and alert when not supported', async () => {
const mockOnAllowCustomValueChange = jest.fn();
const { renderer } = await setup({
...defaultProps,
datasourceSupported: false,
onAllowCustomValueChange: mockOnAllowCustomValueChange,
});
const dataSourcePicker = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect
);
const allowCustomValueCheckbox = renderer.queryByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
const alertText = renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText);
expect(dataSourcePicker).toBeInTheDocument();
expect(allowCustomValueCheckbox).not.toBeInTheDocument();
expect(alertText).toBeInTheDocument();
});
}); });

@ -3,8 +3,9 @@ import { FormEvent, useCallback } from 'react';
import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data'; import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Trans, useTranslate } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { EditorField } from '@grafana/plugin-ui';
import { DataSourceRef } from '@grafana/schema'; import { DataSourceRef } from '@grafana/schema';
import { Alert, CodeEditor, Field, Switch } from '@grafana/ui'; import { Alert, Box, CodeEditor, Field, Switch } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { VariableCheckboxField } from './VariableCheckboxField'; import { VariableCheckboxField } from './VariableCheckboxField';
@ -18,6 +19,8 @@ export interface GroupByVariableFormProps {
defaultOptions?: MetricFindValue[]; defaultOptions?: MetricFindValue[];
allowCustomValue: boolean; allowCustomValue: boolean;
onAllowCustomValueChange: (event: FormEvent<HTMLInputElement>) => void; onAllowCustomValueChange: (event: FormEvent<HTMLInputElement>) => void;
inline?: boolean;
datasourceSupported: boolean;
} }
export function GroupByVariableForm({ export function GroupByVariableForm({
@ -28,6 +31,8 @@ export function GroupByVariableForm({
onDefaultOptionsChange, onDefaultOptionsChange,
allowCustomValue, allowCustomValue,
onAllowCustomValueChange, onAllowCustomValueChange,
inline,
datasourceSupported,
}: GroupByVariableFormProps) { }: GroupByVariableFormProps) {
const updateDefaultOptions = useCallback( const updateDefaultOptions = useCallback(
(csvContent: string) => { (csvContent: string) => {
@ -45,70 +50,85 @@ export function GroupByVariableForm({
return ( return (
<> <>
<VariableLegend> {!inline && (
<Trans i18nKey="dashboard-scene.group-by-variable-form.group-by-options">Group by options</Trans> <VariableLegend>
</VariableLegend> <Trans i18nKey="dashboard-scene.group-by-variable-form.group-by-options">Group by options</Trans>
<Field </VariableLegend>
label={t('dashboard-scene.group-by-variable-form.label-data-source', 'Data source')} )}
htmlFor="data-source-picker"
> <Box marginBottom={2}>
<DataSourcePicker current={datasource} onChange={onDataSourceChange} width={30} variables={true} noDefault /> <EditorField
</Field> label={t('dashboard-scene.group-by-variable-form.label-data-source', 'Data source')}
htmlFor="data-source-picker"
tooltip={infoText}
>
<DataSourcePicker current={datasource} onChange={onDataSourceChange} width={30} variables={true} noDefault />
</EditorField>
</Box>
{infoText ? ( {!datasourceSupported ? (
<Alert <Alert
title={infoText} title={t(
severity="info" 'dashboard-scene.group-by-variable-form.alert-not-supported',
'This data source does not support group by variables'
)}
severity="warning"
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText} data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText}
/> />
) : null} ) : null}
<Field {datasourceSupported && (
label={t( <>
'dashboard-scene.group-by-variable-form.label-use-static-group-by-dimensions', <Field
'Use static group dimensions' label={t(
)} 'dashboard-scene.group-by-variable-form.label-use-static-group-by-dimensions',
description={t( 'Use static group dimensions'
'dashboard-scene.group-by-variable-form.description-provide-dimensions-as-csv-dimension-name-dimension-id', )}
'Provide dimensions as CSV: {{name}}, {{value}}', description={t(
{ name: 'dimensionName', value: 'dimensionId' } 'dashboard-scene.group-by-variable-form.description-provide-dimensions-as-csv-dimension-name-dimension-id',
)} 'Provide dimensions as CSV: {{name}}, {{value}}',
> { name: 'dimensionName', value: 'dimensionId' }
<Switch )}
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle} >
value={defaultOptions !== undefined} <Switch
onChange={(e) => { data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle}
if (defaultOptions === undefined) { value={defaultOptions !== undefined}
onDefaultOptionsChange([]); onChange={(e) => {
} else { if (defaultOptions === undefined) {
onDefaultOptionsChange(undefined); onDefaultOptionsChange([]);
} } else {
}} onDefaultOptionsChange(undefined);
/> }
</Field> }}
/>
</Field>
{defaultOptions !== undefined && ( {defaultOptions !== undefined && (
<CodeEditor <CodeEditor
height={300} height={300}
language="csv" language="csv"
value={defaultOptions.map((o) => `${o.text},${o.value}`).join('\n')} value={defaultOptions.map((o) => `${o.text},${o.value}`).join('\n')}
onBlur={updateDefaultOptions} onBlur={updateDefaultOptions}
onSave={updateDefaultOptions} onSave={updateDefaultOptions}
showMiniMap={false} showMiniMap={false}
showLineNumbers={true} showLineNumbers={true}
/> />
)}
</>
)} )}
<VariableCheckboxField {datasourceSupported && !inline && onAllowCustomValueChange && (
value={allowCustomValue} <VariableCheckboxField
name="Allow custom values" value={allowCustomValue}
description={t( name="Allow custom values"
'dashboard-scene.group-by-variable-form.description-enables-users-custom-values', description={t(
'Enables users to add custom values to the list' 'dashboard-scene.group-by-variable-form.description-enables-users-custom-values',
)} 'Enables users to add custom values to the list'
onChange={onAllowCustomValueChange} )}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch} onChange={onAllowCustomValueChange}
/> testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
/>
)}
</> </>
); );
} }

@ -1,13 +1,14 @@
import { act, render } from '@testing-library/react'; import { act, render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { MetricFindValue, VariableSupportType } from '@grafana/data'; import { MetricFindValue, VariableSupportType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { GroupByVariable } from '@grafana/scenes'; import { GroupByVariable } from '@grafana/scenes';
import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
import { GroupByVariableEditor } from './GroupByVariableEditor'; import { getGroupByVariableOptions, GroupByVariableEditor } from './GroupByVariableEditor';
const defaultDatasource = mockDataSource({ const defaultDatasource = mockDataSource({
name: 'Default Test Data Source', name: 'Default Test Data Source',
@ -31,6 +32,7 @@ jest.mock('@grafana/runtime', () => ({
query: jest.fn(), query: jest.fn(),
editor: jest.fn().mockImplementation(LegacyVariableQueryEditor), editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
}, },
getTagKeys: () => [],
}), }),
getList: () => [defaultDatasource, promDatasource], getList: () => [defaultDatasource, promDatasource],
getInstanceSettings: () => ({ ...defaultDatasource }), getInstanceSettings: () => ({ ...defaultDatasource }),
@ -43,8 +45,8 @@ describe('GroupByVariableEditor', () => {
const dataSourcePicker = renderer.getByTestId( const dataSourcePicker = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect
); );
const infoText = renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText);
const allowCustomValueCheckbox = renderer.getByTestId( const allowCustomValueCheckbox = renderer.queryByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
); );
@ -52,8 +54,6 @@ describe('GroupByVariableEditor', () => {
expect(allowCustomValueCheckbox).toBeChecked(); expect(allowCustomValueCheckbox).toBeChecked();
expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker).toBeInTheDocument();
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
expect(infoText).toBeInTheDocument();
expect(infoText).toHaveTextContent('This data source does not support group by variable yet.');
}); });
it('should update the variable data source when data source picker is changed', async () => { it('should update the variable data source when data source picker is changed', async () => {
@ -87,6 +87,31 @@ describe('GroupByVariableEditor', () => {
expect(variable.state.defaultOptions).toEqual(undefined); expect(variable.state.defaultOptions).toEqual(undefined);
}); });
it('should return an OptionsPaneItemDescriptor that renders Editor', async () => {
const variable = new GroupByVariable({
name: 'test',
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
});
const result = getGroupByVariableOptions(variable);
expect(result.length).toBe(1);
const descriptor = result[0];
// Mock the parent property that OptionsPaneItem expects
descriptor.parent = new OptionsPaneCategoryDescriptor({
id: 'mock-parent-id',
title: 'Mock Parent',
});
render(descriptor.render());
await waitFor(() => {
// Check that some part of the component renders
expect(screen.getByText(/data source does not support/i)).toBeInTheDocument();
});
});
}); });
async function setup(defaultOptions?: MetricFindValue[]) { async function setup(defaultOptions?: MetricFindValue[]) {

@ -1,26 +1,30 @@
import { noop } from 'lodash';
import { FormEvent } from 'react'; import { FormEvent } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data'; import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { GroupByVariable } from '@grafana/scenes'; import { GroupByVariable, SceneVariable } from '@grafana/scenes';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { GroupByVariableForm } from '../components/GroupByVariableForm'; import { GroupByVariableForm } from '../components/GroupByVariableForm';
interface GroupByVariableEditorProps { interface GroupByVariableEditorProps {
variable: GroupByVariable; variable: GroupByVariable;
onRunQuery: () => void; onRunQuery: () => void;
inline?: boolean;
} }
export function GroupByVariableEditor(props: GroupByVariableEditorProps) { export function GroupByVariableEditor(props: GroupByVariableEditorProps) {
const { variable, onRunQuery } = props; const { variable, onRunQuery, inline } = props;
const { datasource: datasourceRef, defaultOptions, allowCustomValue = true } = variable.useState(); const { datasource: datasourceRef, defaultOptions, allowCustomValue = true } = variable.useState();
const { value: datasource } = useAsync(async () => { const { value: datasource } = useAsync(async () => {
return await getDataSourceSrv().get(datasourceRef); return await getDataSourceSrv().get(datasourceRef);
}, [variable.state]); }, [variable.state]);
const message = datasource?.getTagKeys const supported = datasource?.getTagKeys !== undefined;
const message = supported
? 'Group by dimensions are applied automatically to all queries that target this data source' ? 'Group by dimensions are applied automatically to all queries that target this data source'
: 'This data source does not support group by variable yet.'; : 'This data source does not support group by variable yet.';
@ -49,6 +53,21 @@ export function GroupByVariableEditor(props: GroupByVariableEditorProps) {
onDefaultOptionsChange={onDefaultOptionsChange} onDefaultOptionsChange={onDefaultOptionsChange}
allowCustomValue={allowCustomValue} allowCustomValue={allowCustomValue}
onAllowCustomValueChange={onAllowCustomValueChange} onAllowCustomValueChange={onAllowCustomValueChange}
inline={inline}
datasourceSupported={supported}
/> />
); );
} }
export function getGroupByVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
if (!(variable instanceof GroupByVariable)) {
console.warn('getAdHocFilterOptions: variable is not an AdHocFiltersVariable');
return [];
}
return [
new OptionsPaneItemDescriptor({
render: () => <GroupByVariableEditor variable={variable} onRunQuery={noop} inline={true} />,
}),
];
}

@ -27,7 +27,7 @@ import { AdHocFiltersVariableEditor, getAdHocFilterOptions } from './editors/AdH
import { ConstantVariableEditor, getConstantVariableOptions } from './editors/ConstantVariableEditor'; import { ConstantVariableEditor, getConstantVariableOptions } from './editors/ConstantVariableEditor';
import { CustomVariableEditor, getCustomVariableOptions } from './editors/CustomVariableEditor'; import { CustomVariableEditor, getCustomVariableOptions } from './editors/CustomVariableEditor';
import { DataSourceVariableEditor, getDataSourceVariableOptions } from './editors/DataSourceVariableEditor'; import { DataSourceVariableEditor, getDataSourceVariableOptions } from './editors/DataSourceVariableEditor';
import { GroupByVariableEditor } from './editors/GroupByVariableEditor'; import { getGroupByVariableOptions, GroupByVariableEditor } from './editors/GroupByVariableEditor';
import { getIntervalVariableOptions, IntervalVariableEditor } from './editors/IntervalVariableEditor'; import { getIntervalVariableOptions, IntervalVariableEditor } from './editors/IntervalVariableEditor';
import { getQueryVariableOptions, QueryVariableEditor } from './editors/QueryVariableEditor'; import { getQueryVariableOptions, QueryVariableEditor } from './editors/QueryVariableEditor';
import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/TextBoxVariableEditor'; import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/TextBoxVariableEditor';
@ -87,6 +87,7 @@ export const EDITABLE_VARIABLES: Record<EditableVariableType, EditableVariableCo
name: 'Group by', name: 'Group by',
description: 'Add keys to group by on the fly', description: 'Add keys to group by on the fly',
editor: GroupByVariableEditor, editor: GroupByVariableEditor,
getOptions: getGroupByVariableOptions,
}, },
textbox: { textbox: {
name: 'Textbox', name: 'Textbox',

@ -4964,6 +4964,7 @@
} }
}, },
"group-by-variable-form": { "group-by-variable-form": {
"alert-not-supported": "This data source does not support group by variables",
"description-enables-users-custom-values": "Enables users to add custom values to the list", "description-enables-users-custom-values": "Enables users to add custom values to the list",
"description-provide-dimensions-as-csv-dimension-name-dimension-id": "Provide dimensions as CSV: {{name}}, {{value}}", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Provide dimensions as CSV: {{name}}, {{value}}",
"group-by-options": "Group by options", "group-by-options": "Group by options",

Loading…
Cancel
Save