From 3175a04f4529a629677d184ae71eac8f1fb27aa0 Mon Sep 17 00:00:00 2001 From: Scott Lepper Date: Wed, 14 May 2025 20:31:49 -0400 Subject: [PATCH] Dashboards: Edit Pane - Ad Hoc Filter Variables (#105304) Dashboards: Edit Pane - Ad Hoc Filter Variables --- .../components/AdHocVariableForm.test.tsx | 38 ++++++++++++--- .../components/AdHocVariableForm.tsx | 44 +++++++++++------ .../AdHocFiltersVariableEditor.test.tsx | 48 ++++++++++++++++--- .../editors/AdHocFiltersVariableEditor.tsx | 22 ++++++++- .../editors/QueryVariableEditor.test.tsx | 4 +- .../variables/editors/QueryVariableEditor.tsx | 2 +- .../settings/variables/utils.ts | 3 +- .../variables/adhoc/AdHocVariableEditor.tsx | 1 + public/locales/en-US/grafana.json | 2 + 9 files changed, 130 insertions(+), 34 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx index 61e9ee8b835..1b0b2ed3f06 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx @@ -34,6 +34,7 @@ describe('AdHocVariableForm', () => { datasource: defaultDatasource, onDataSourceChange, infoText: 'Test Info', + datasourceSupported: true, }; it('should render the form with the provided data source', async () => { @@ -42,14 +43,9 @@ describe('AdHocVariableForm', () => { const dataSourcePicker = renderer.getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect ); - const infoText = renderer.getByTestId( - selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText - ); expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); - expect(infoText).toBeInTheDocument(); - expect(infoText).toHaveTextContent('Test Info'); }); it('should call the onDataSourceChange callback when the data source is changed', async () => { @@ -116,6 +112,7 @@ describe('AdHocVariableForm', () => { ...defaultProps, defaultKeys: [{ text: 'test', value: 'test' }], onDefaultKeysChange: mockOnStaticKeysChange, + datasourceSupported: true, }); await userEvent.click( @@ -124,11 +121,40 @@ describe('AdHocVariableForm', () => { expect(mockOnStaticKeysChange).toHaveBeenCalledTimes(1); expect(mockOnStaticKeysChange).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.AdHocFiltersVariable.datasourceSelect + ); + + const allowCustomValueCheckbox = renderer.queryByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch + ); + + const alertText = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText + ); + + expect(dataSourcePicker).toBeInTheDocument(); + expect(allowCustomValueCheckbox).not.toBeInTheDocument(); + expect(alertText).toBeInTheDocument(); + }); }); async function setup(props?: React.ComponentProps) { return { - renderer: await act(() => render()), + renderer: await act(() => + render( + + ) + ), user: userEvent.setup(), }; } diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx index c6d4bd39724..80214944050 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -2,8 +2,9 @@ import { FormEvent, useCallback } from 'react'; import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { EditorField } from '@grafana/plugin-ui'; import { DataSourceRef } from '@grafana/schema'; -import { Alert, CodeEditor, Field, Switch } from '@grafana/ui'; +import { Alert, CodeEditor, Field, Switch, Box } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; @@ -18,6 +19,8 @@ export interface AdHocVariableFormProps { defaultKeys?: MetricFindValue[]; onDefaultKeysChange?: (keys?: MetricFindValue[]) => void; onAllowCustomValueChange?: (event: FormEvent) => void; + inline?: boolean; + datasourceSupported: boolean; } export function AdHocVariableForm({ @@ -28,6 +31,8 @@ export function AdHocVariableForm({ onDefaultKeysChange, onAllowCustomValueChange, defaultKeys, + inline, + datasourceSupported, }: AdHocVariableFormProps) { const updateStaticKeys = useCallback( (csvContent: string) => { @@ -44,25 +49,34 @@ export function AdHocVariableForm({ return ( <> - - Ad-hoc options - - - - + {!inline && ( + + Ad-hoc options + + )} - {infoText ? ( + + + + + + + {datasourceSupported === false ? ( ) : null} - {onDefaultKeysChange && ( + {datasourceSupported && onDefaultKeysChange && ( <> )} - {onAllowCustomValueChange && ( + {datasourceSupported && onAllowCustomValueChange && ( []; + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getDataSourceSrv: () => ({ @@ -41,6 +44,7 @@ jest.mock('@grafana/runtime', () => ({ query: jest.fn(), editor: jest.fn().mockImplementation(LegacyVariableQueryEditor), }, + getTagKeys: getTagKeysMock, }), getList: () => [defaultDatasource, promDatasource], getInstanceSettings: () => ({ ...defaultDatasource }), @@ -62,24 +66,28 @@ const runRequestMock = jest.fn().mockReturnValue( setRunRequest(runRequestMock); describe('AdHocFiltersVariableEditor', () => { + beforeEach(() => { + getTagKeysMock = () => []; + }); + it('renders AdHocVariableForm with correct props', async () => { + getTagKeysMock = undefined; + const { renderer } = await setup(); const dataSourcePicker = renderer.getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect ); - const infoText = renderer.getByTestId( + const infoText = renderer.queryByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText ); - const allowCustomValueCheckbox = renderer.getByTestId( + const allowCustomValueCheckbox = renderer.queryByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch ); - expect(allowCustomValueCheckbox).toBeInTheDocument(); - expect(allowCustomValueCheckbox).toBeChecked(); + expect(allowCustomValueCheckbox).not.toBeInTheDocument(); expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(infoText).toBeInTheDocument(); - expect(infoText).toHaveTextContent('This data source does not support ad hoc filters yet.'); }); it('should update the variable data source when data source picker is changed', async () => { @@ -104,6 +112,7 @@ describe('AdHocFiltersVariableEditor', () => { }); it('should update the variable default keys when the default keys option is disabled', async () => { + getTagKeysMock = () => Promise.resolve(['key1', 'key2']); const { renderer, variable, user } = await setup(undefined, true); // Simulate toggling default options off @@ -113,6 +122,31 @@ describe('AdHocFiltersVariableEditor', () => { expect(variable.state.defaultKeys).toEqual(undefined); }); + + it('should return an OptionsPaneItemDescriptor that renders Editor', async () => { + const variable = new AdHocFiltersVariable({ + name: 'test', + datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type }, + }); + + const result = getAdHocFilterOptions(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(props?: React.ComponentProps, withDefaultKeys = false) { diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx index cedb3bad6da..f35b9ac1ead 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx @@ -1,15 +1,18 @@ +import { noop } from 'lodash'; import { FormEvent } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { AdHocFiltersVariable } from '@grafana/scenes'; +import { AdHocFiltersVariable, SceneVariable } from '@grafana/scenes'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { AdHocVariableForm } from '../components/AdHocVariableForm'; interface AdHocFiltersVariableEditorProps { variable: AdHocFiltersVariable; onRunQuery: (variable: AdHocFiltersVariable) => void; + inline?: boolean; } export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) { @@ -22,7 +25,7 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp const message = datasourceSettings?.getTagKeys ? 'Ad hoc filters are applied automatically to all queries that target this data source' - : 'This data source does not support ad hoc filters yet.'; + : 'This data source does not support ad hoc filters.'; const onDataSourceChange = (ds: DataSourceInstanceSettings) => { const dsRef = getDataSourceRef(ds); @@ -52,6 +55,21 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp defaultKeys={defaultKeys} onDefaultKeysChange={onDefaultKeysChange} onAllowCustomValueChange={onAllowCustomValueChange} + inline={props.inline} + datasourceSupported={datasourceSettings?.getTagKeys ? true : false} /> ); } + +export function getAdHocFilterOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] { + if (!(variable instanceof AdHocFiltersVariable)) { + console.warn('getAdHocFilterOptions: variable is not an AdHocFiltersVariable'); + return []; + } + + return [ + new OptionsPaneItemDescriptor({ + render: () => , + }), + ]; +} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 97823f9f493..9dd39275c62 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -406,7 +406,7 @@ describe('QueryVariableEditor', () => { // 3. Assert Editor's key elements are rendered // DataSourcePicker's Field - expect(within(modal).getByLabelText('Data source')).toBeInTheDocument(); + expect(within(modal).getByLabelText('Target data source')).toBeInTheDocument(); // Regex input placeholder expect(within(modal).getByPlaceholderText(/text>.*value/i)).toBeInTheDocument(); // Sort select (check for its current value display) @@ -451,7 +451,7 @@ describe('Editor', () => { render(); }); - const dataSourcePicker = screen.getByLabelText('Data source'); + const dataSourcePicker = screen.getByLabelText('Target data source'); expect(dataSourcePicker).toBeInTheDocument(); const user = userEvent.setup(); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index d218fd5f530..27930e977c5 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -196,7 +196,7 @@ export function Editor({ variable }: { variable: QueryVariable }) { return ( <> diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index e14f5676b3e..f006b0f2221 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -24,7 +24,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { getIntervalsQueryFromNewIntervalModel } from '../../utils/utils'; import { getCustomVariableOptions } from './components/CustomVariableForm'; -import { AdHocFiltersVariableEditor } from './editors/AdHocFiltersVariableEditor'; +import { AdHocFiltersVariableEditor, getAdHocFilterOptions } from './editors/AdHocFiltersVariableEditor'; import { ConstantVariableEditor, getConstantVariableOptions } from './editors/ConstantVariableEditor'; import { CustomVariableEditor } from './editors/CustomVariableEditor'; import { DataSourceVariableEditor } from './editors/DataSourceVariableEditor'; @@ -81,6 +81,7 @@ export const EDITABLE_VARIABLES: Record { datasource={variable.datasource ?? undefined} onDataSourceChange={this.onDatasourceChanged} infoText={extended?.infoText} + datasourceSupported={variable.datasource === undefined ? false : true} // legacy behavior - will show data source settings even if not supported /> ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 19f5eb75a63..cfa1eb21df5 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4020,6 +4020,7 @@ "dashboard-scene": { "ad-hoc-variable-form": { "adhoc-options": "Ad-hoc options", + "alert-not-supported": "This data source does not support ad hoc filters", "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}}", "label-data-source": "Data source", @@ -4244,6 +4245,7 @@ "description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples).", "description-optional": "Optional, if you want to extract part of a series name or metric node segment.", "label-data-source": "Data source", + "label-target-data-source": "Target data source", "query-options": "Query options", "selection-options": "Selection options" },