diff --git a/package.json b/package.json index 20ca52b91ff..90add7b5a5f 100644 --- a/package.json +++ b/package.json @@ -250,7 +250,7 @@ "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^3.11.0", + "@grafana/scenes": "3.13.3", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index b088ed63c85..1b01e5b9e53 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -1,4 +1,5 @@ import { LoadingState } from './data'; +import { MetricFindValue } from './datasource'; import { DataSourceRef } from './query'; export type VariableType = TypedVariableModel['type']; @@ -63,6 +64,10 @@ export interface AdHocVariableModel extends BaseVariableModel { * Filters that are always applied to the lookup of keys. Not shown in the AdhocFilterBuilder UI. */ baseFilters?: AdHocVariableFilter[]; + /** + * Static keys that override any dynamic keys from the datasource. + */ + defaultKeys?: MetricFindValue[]; } export interface GroupByVariableModel extends VariableWithOptions { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ae54d0ce941..7b820ee07fc 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -195,6 +195,7 @@ export const Pages = { AdHocFiltersVariable: { datasourceSelect: Components.DataSourcePicker.inputV2, infoText: 'data-testid ad-hoc filters variable info text', + modeToggle: 'data-testid ad-hoc filters variable mode toggle', }, }, }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index e6b1c6576e5..1d3c6c2b333 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -388,6 +388,91 @@ describe('sceneVariablesSetToVariables', () => { "type": "fake-std", "uid": "fake-std", }, + "defaultKeys": undefined, + "description": "test-desc", + "filters": [ + { + "key": "filterTest", + "operator": "=", + "value": "test", + }, + ], + "label": "test-label", + "name": "test", + "type": "adhoc", + } + `); + }); + + it('should handle AdHocFiltersVariable with defaultKeys', () => { + const variable = new AdHocFiltersVariable({ + name: 'test', + label: 'test-label', + description: 'test-desc', + datasource: { uid: 'fake-std', type: 'fake-std' }, + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + }); + const set = new SceneVariableSet({ + variables: [variable], + }); + + const result = sceneVariablesSetToVariables(set); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchInlineSnapshot(` + { + "baseFilters": [ + { + "key": "baseFilterTest", + "operator": "=", + "value": "test", + }, + ], + "datasource": { + "type": "fake-std", + "uid": "fake-std", + }, + "defaultKeys": [ + { + "text": "some", + "value": "1", + }, + { + "text": "static", + "value": "2", + }, + { + "text": "keys", + "value": "3", + }, + ], "description": "test-desc", "filters": [ { diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 28ff40870b3..5f3813c2705 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -124,12 +124,13 @@ export function sceneVariablesSetToVariables(set: SceneVariables) { } else if (sceneUtils.isAdHocVariable(variable)) { variables.push({ ...commonProperties, - name: variable.state.name!, + name: variable.state.name, type: 'adhoc', datasource: variable.state.datasource, // @ts-expect-error baseFilters: variable.state.baseFilters, filters: variable.state.filters, + defaultKeys: variable.state.defaultKeys, }); } else { throw new Error('Unsupported variable type'); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index 2d5d1fe1b0c..2d937285f1c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -965,6 +965,88 @@ describe('transformSaveModelToScene', () => { }); }); + it('should migrate adhoc variable with default keys', () => { + const variable: TypedVariableModel = { + id: 'adhoc', + global: false, + index: 0, + state: LoadingState.Done, + error: null, + name: 'adhoc', + label: 'Adhoc Label', + description: 'Adhoc Description', + type: 'adhoc', + rootStateKey: 'N4XLmH5Vz', + datasource: { + uid: 'gdev-prometheus', + type: 'prometheus', + }, + filters: [ + { + key: 'filterTest', + operator: '=', + value: 'test', + }, + ], + baseFilters: [ + { + key: 'baseFilterTest', + operator: '=', + value: 'test', + }, + ], + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + hide: 0, + skipUrlSync: false, + }; + + const migrated = createSceneVariableFromVariableModel(variable) as AdHocFiltersVariable; + const filterVarState = migrated.state; + + expect(migrated).toBeInstanceOf(AdHocFiltersVariable); + expect(filterVarState).toEqual({ + key: expect.any(String), + description: 'Adhoc Description', + hide: 0, + label: 'Adhoc Label', + name: 'adhoc', + skipUrlSync: false, + type: 'adhoc', + filterExpression: 'filterTest="test"', + filters: [{ key: 'filterTest', operator: '=', value: 'test' }], + baseFilters: [{ key: 'baseFilterTest', operator: '=', value: 'test' }], + datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, + applyMode: 'auto', + defaultKeys: [ + { + text: 'some', + value: '1', + }, + { + text: 'static', + value: '2', + }, + { + text: 'keys', + value: '3', + }, + ], + }); + }); + describe('when groupByVariable feature toggle is enabled', () => { beforeAll(() => { config.featureToggles.groupByVariable = true; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index d2c415eced9..015146d7b5d 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -344,6 +344,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode applyMode: 'auto', filters: variable.filters ?? [], baseFilters: variable.baseFilters ?? [], + defaultKeys: variable.defaultKeys, }); } if (variable.type === 'custom') { 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 498483266d4..610a2a9eea9 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 @@ -1,11 +1,11 @@ -import { act, render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; -import { AdHocVariableForm } from './AdHocVariableForm'; +import { AdHocVariableForm, AdHocVariableFormProps } from './AdHocVariableForm'; const defaultDatasource = mockDataSource({ name: 'Default Test Data Source', @@ -29,13 +29,15 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ })); describe('AdHocVariableForm', () => { + const onDataSourceChange = jest.fn(); + const defaultProps: AdHocVariableFormProps = { + datasource: defaultDatasource, + onDataSourceChange, + infoText: 'Test Info', + }; + it('should render the form with the provided data source', async () => { - const onDataSourceChange = jest.fn(); - const { renderer } = await setup({ - datasource: defaultDatasource, - onDataSourceChange, - infoText: 'Test Info', - }); + const { renderer } = await setup(defaultProps); const dataSourcePicker = renderer.getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect @@ -51,12 +53,7 @@ describe('AdHocVariableForm', () => { }); it('should call the onDataSourceChange callback when the data source is changed', async () => { - const onDataSourceChange = jest.fn(); - const { renderer, user } = await setup({ - datasource: defaultDatasource, - onDataSourceChange, - infoText: 'Test Info', - }); + const { renderer, user } = await setup(defaultProps); // Simulate changing the data source await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2)); @@ -65,6 +62,52 @@ describe('AdHocVariableForm', () => { expect(onDataSourceChange).toHaveBeenCalledTimes(1); expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); }); + + it('should not render code editor when no default keys provided', async () => { + await setup(defaultProps); + + expect(screen.queryByTestId(selectors.components.CodeEditor.container)).not.toBeInTheDocument(); + }); + + it('should render code editor when defaultKeys and onDefaultKeysChange are provided', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + defaultKeys: [{ text: 'test', value: 'test' }], + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + expect(await screen.findByTestId(selectors.components.CodeEditor.container)).toBeInTheDocument(); + }); + + it('should call onDefaultKeysChange when toggling on default options', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + await userEvent.click( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + expect(mockOnStaticKeysChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticKeysChange).toHaveBeenCalledWith([]); + }); + + it('should call onDefaultKeysChange when toggling off default options', async () => { + const mockOnStaticKeysChange = jest.fn(); + await setup({ + ...defaultProps, + defaultKeys: [{ text: 'test', value: 'test' }], + onDefaultKeysChange: mockOnStaticKeysChange, + }); + + await userEvent.click( + screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + expect(mockOnStaticKeysChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticKeysChange).toHaveBeenCalledWith(undefined); + }); }); async function setup(props?: React.ComponentProps) { 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 fe093a380b5..415e9f6cc21 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -1,20 +1,41 @@ -import React from 'react'; +import React, { useCallback } from 'react'; -import { DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { DataSourceRef } from '@grafana/schema'; -import { Alert, Field } from '@grafana/ui'; +import { Alert, CodeEditor, Field, Switch } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { VariableLegend } from './VariableLegend'; -interface AdHocVariableFormProps { +export interface AdHocVariableFormProps { datasource?: DataSourceRef; onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; infoText?: string; + defaultKeys?: MetricFindValue[]; + onDefaultKeysChange?: (keys?: MetricFindValue[]) => void; } -export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: AdHocVariableFormProps) { +export function AdHocVariableForm({ + datasource, + infoText, + onDataSourceChange, + onDefaultKeysChange, + defaultKeys, +}: AdHocVariableFormProps) { + const updateStaticKeys = useCallback( + (csvContent: string) => { + const df = readCSV('key,value\n' + csvContent)[0]; + const options = []; + for (let i = 0; i < df.length; i++) { + options.push({ text: df.fields[0].values[i], value: df.fields[1].values[i] }); + } + + onDefaultKeysChange?.(options); + }, + [onDefaultKeysChange] + ); + return ( <> Ad-hoc options @@ -29,6 +50,36 @@ export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText} /> ) : null} + + {onDefaultKeysChange && ( + <> + + { + if (defaultKeys === undefined) { + onDefaultKeysChange([]); + } else { + onDefaultKeysChange(undefined); + } + }} + /> + + + {defaultKeys !== undefined && ( + `${o.text},${o.value}`).join('\n')} + onBlur={updateStaticKeys} + onSave={updateStaticKeys} + showMiniMap={false} + showLineNumbers={true} + /> + )} + + )} ); } diff --git a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx index 194ff364174..f982ab92361 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx @@ -51,10 +51,7 @@ export function GroupByVariableForm({ /> ) : null} - + { expect(variable.state.datasource).toEqual({ uid: 'prometheus', type: 'prometheus' }); }); + + it('should update the variable default keys when the default keys options is enabled', async () => { + const { renderer, variable, user } = await setup(); + + // Simulate toggling default options on + await user.click( + renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + + expect(variable.state.defaultKeys).toEqual([]); + }); + + it('should update the variable default keys when the default keys option is disabled', async () => { + const { renderer, variable, user } = await setup(undefined, true); + + // Simulate toggling default options off + await user.click( + renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.modeToggle) + ); + + expect(variable.state.defaultKeys).toEqual(undefined); + }); }); -async function setup(props?: React.ComponentProps) { +async function setup(props?: React.ComponentProps, withDefaultKeys = false) { const onRunQuery = jest.fn(); const variable = new AdHocFiltersVariable({ name: 'adhocVariable', @@ -110,6 +132,7 @@ async function setup(props?: React.ComponentProps 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 37daff7cb56..8aa2a2a6daf 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useAsync } from 'react-use'; -import { DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, MetricFindValue } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; @@ -15,7 +15,7 @@ interface AdHocFiltersVariableEditorProps { export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) { const { variable } = props; - const datasourceRef = variable.useState().datasource ?? undefined; + const { datasource: datasourceRef, defaultKeys } = variable.useState(); const { value: datasourceSettings } = useAsync(async () => { return await getDataSourceSrv().get(datasourceRef); @@ -36,5 +36,19 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp }); }; - return ; + const onDefaultKeysChange = (defaultKeys?: MetricFindValue[]) => { + variable.setState({ + defaultKeys, + }); + }; + + return ( + + ); } diff --git a/yarn.lock b/yarn.lock index b696f82af62..892d379d04d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4037,9 +4037,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^3.11.0": - version: 3.11.0 - resolution: "@grafana/scenes@npm:3.11.0" +"@grafana/scenes@npm:3.13.3": + version: 3.13.3 + resolution: "@grafana/scenes@npm:3.13.3" dependencies: "@grafana/e2e-selectors": "npm:10.3.3" react-grid-layout: "npm:1.3.4" @@ -4053,7 +4053,7 @@ __metadata: "@grafana/ui": ^10.0.3 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/47629dd3f5129b8f803d54c512d10f921edf9b138b878fbebe664e2537d6813c72ea5119e28216daf7e426ef764400356db9b1532c601a3e029ae12baceb248d + checksum: 10/5b7f2e2714dcdbc3ad58352ec0cc7f513f7a240dc11a3e309733a662760a598e9333ec377f2899b6b668e018c2e885e8a044af7d42d1be4487d692cda3b9359a languageName: node linkType: hard @@ -18336,7 +18336,7 @@ __metadata: "@grafana/plugin-e2e": "npm:^0.21.0" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^3.11.0" + "@grafana/scenes": "npm:3.13.3" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1"