Dashboards: Use `auto` and only use `AdHocFiltersVariable` to manage filters (#81318)

pull/82399/head
Ivan Ortega Alba 2 years ago committed by GitHub
parent 63670b7adc
commit 3d86d101b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      package.json
  2. 4
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  3. 20
      public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts
  4. 42
      public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
  5. 36
      public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
  6. 57
      public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
  7. 10
      public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
  8. 69
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts
  9. 33
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  10. 12
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts
  11. 47
      public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx
  12. 36
      public/app/features/dashboard-scene/settings/VariablesEditView.tsx
  13. 75
      public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx
  14. 34
      public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx
  15. 122
      public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx
  16. 32
      public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
  17. 6
      public/app/features/dashboard-scene/settings/variables/utils.test.ts
  18. 5
      public/app/features/dashboard-scene/settings/variables/utils.ts
  19. 2
      public/app/features/dashboard-scene/utils/getVariablesCompatibility.ts
  20. 4
      public/app/features/trails/ActionTabs/AddToFiltersGraphAction.tsx
  21. 2
      public/app/features/trails/ActionTabs/utils.ts
  22. 4
      public/app/features/trails/DataTrail.tsx
  23. 2
      public/app/features/trails/DataTrailCard.tsx
  24. 7
      public/app/features/trails/TrailStore/TrailStore.test.ts
  25. 90
      public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx
  26. 24
      public/app/features/variables/adhoc/AdHocVariableEditor.tsx
  27. 10
      yarn.lock

@ -246,7 +246,7 @@
"@grafana/o11y-ds-frontend": "workspace:*",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^2.6.5",
"@grafana/scenes": "^3.2.1",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",

@ -182,6 +182,10 @@ export const Pages = {
stepCountIntervalSelect: 'data-testid interval variable step count input',
minIntervalInput: 'data-testid interval variable mininum interval input',
},
AdHocFiltersVariable: {
datasourceSelect: Components.DataSourcePicker.inputV2,
infoText: 'data-testid ad-hoc filters variable info text',
},
},
},
},

@ -5,7 +5,7 @@ import { PanelContext } from '@grafana/ui';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { findVizPanelByKey } from '../utils/utils';
import { getAdHocFilterSetFor, setDashboardPanelContext } from './setDashboardPanelContext';
import { getAdHocFilterVariableFor, setDashboardPanelContext } from './setDashboardPanelContext';
const postFn = jest.fn();
const putFn = jest.fn();
@ -132,26 +132,26 @@ describe('setDashboardPanelContext', () => {
context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' });
const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' });
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
expect(set.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]);
expect(variable.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]);
});
it('Should update and add filter to existing set', () => {
const { scene, context } = buildTestScene({ existingFilterSet: true });
const { scene, context } = buildTestScene({ existingFilterVariable: true });
const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' });
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
set.setState({ filters: [{ key: 'existing', value: 'world', operator: '=' }] });
variable.setState({ filters: [{ key: 'existing', value: 'world', operator: '=' }] });
context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' });
expect(set.state.filters.length).toBe(2);
expect(variable.state.filters.length).toBe(2);
// Can update existing filter value without adding a new filter
context.onAddAdHocFilter!({ key: 'hello', value: 'world2', operator: '=' });
// Verify existing filter value updated
expect(set.state.filters[1].value).toBe('world2');
expect(variable.state.filters[1].value).toBe('world2');
});
});
});
@ -163,7 +163,7 @@ interface SceneOptions {
canEdit?: boolean;
canDelete?: boolean;
orgCanEdit?: boolean;
existingFilterSet?: boolean;
existingFilterVariable?: boolean;
}
function buildTestScene(options: SceneOptions) {
@ -198,7 +198,7 @@ function buildTestScene(options: SceneOptions) {
},
],
templating: {
list: options.existingFilterSet
list: options.existingFilterVariable
? [
{
type: 'adhoc',

@ -1,5 +1,5 @@
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data';
import { AdHocFilterSet, dataLayers, SceneDataLayers, VizPanel } from '@grafana/scenes';
import { AdHocFiltersVariable, dataLayers, SceneDataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api';
@ -111,8 +111,8 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
return;
}
const filterSet = getAdHocFilterSetFor(dashboard, queryRunner.state.datasource);
updateAdHocFilterSet(filterSet, newFilter);
const filterVar = getAdHocFilterVariableFor(dashboard, queryRunner.state.datasource);
updateAdHocFilterVariable(filterVar, newFilter);
};
context.onUpdateData = (frames: DataFrame[]): Promise<boolean> => {
@ -149,33 +149,37 @@ function reRunBuiltInAnnotationsLayer(scene: DashboardScene) {
}
}
export function getAdHocFilterSetFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) {
const controls = scene.state.controls ?? [];
export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) {
const variables = sceneGraph.getVariables(scene);
for (const control of controls) {
if (control instanceof AdHocFilterSet) {
if (control.state.datasource === ds || control.state.datasource?.uid === ds?.uid) {
return control;
for (const variable of variables.state.variables) {
if (sceneUtils.isAdHocVariable(variable)) {
const filtersDs = variable.state.datasource;
if (filtersDs === ds || filtersDs?.uid === ds?.uid) {
return variable;
}
}
}
const newSet = new AdHocFilterSet({ datasource: ds });
const newVariable = new AdHocFiltersVariable({
name: 'Filters',
datasource: ds,
});
// Add it to the scene
scene.setState({
controls: [controls[0], newSet, ...controls.slice(1)],
variables.setState({
variables: [...variables.state.variables, newVariable],
});
return newSet;
return newVariable;
}
function updateAdHocFilterSet(filterSet: AdHocFilterSet, newFilter: AdHocFilterItem) {
function updateAdHocFilterVariable(filterVar: AdHocFiltersVariable, newFilter: AdHocFilterItem) {
// Check if we need to update an existing filter
for (const filter of filterSet.state.filters) {
for (const filter of filterVar.state.filters) {
if (filter.key === newFilter.key) {
filterSet.setState({
filters: filterSet.state.filters.map((f) => {
filterVar.setState({
filters: filterVar.state.filters.map((f) => {
if (f.key === newFilter.key) {
return newFilter;
}
@ -187,7 +191,7 @@ function updateAdHocFilterSet(filterSet: AdHocFilterSet, newFilter: AdHocFilterI
}
// Add new filter
filterSet.setState({
filters: [...filterSet.state.filters, newFilter],
filterVar.setState({
filters: [...filterVar.state.filters, newFilter],
});
}

@ -455,6 +455,16 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"filters": [],
"name": "Filters",
"type": "adhoc",
},
{
"auto": true,
"auto_count": 30,
@ -525,14 +535,6 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"skipUrlSync": true,
"type": "constant",
},
{
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"name": "Filters",
"type": "adhoc",
},
],
},
"time": {
@ -757,6 +759,16 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"filters": [],
"name": "Filters",
"type": "adhoc",
},
{
"auto": true,
"auto_count": 30,
@ -827,14 +839,6 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"skipUrlSync": true,
"type": "constant",
},
{
"datasource": {
"type": "prometheus",
"uid": "wc2AL7L7k",
},
"name": "Filters",
"type": "adhoc",
},
],
},
"time": {

@ -13,6 +13,7 @@ import {
} from '@grafana/data';
import { setRunRequest } from '@grafana/runtime';
import {
AdHocFiltersVariable,
ConstantVariable,
CustomVariable,
DataSourceVariable,
@ -344,4 +345,60 @@ describe('sceneVariablesSetToVariables', () => {
}
`);
});
it('should handle AdHocFiltersVariable', () => {
const variable = new AdHocFiltersVariable({
name: 'test',
label: 'test-label',
description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' },
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",
},
"description": "test-desc",
"filters": [
{
"key": "filterTest",
"operator": "=",
"value": "test",
},
],
"label": "test-label",
"name": "test",
"type": "adhoc",
}
`);
});
});

@ -104,6 +104,16 @@ export function sceneVariablesSetToVariables(set: SceneVariables) {
},
query: variable.state.value,
});
} else if (sceneUtils.isAdHocVariable(variable)) {
variables.push({
...commonProperties,
name: variable.state.name!,
type: 'adhoc',
datasource: variable.state.datasource,
// @ts-expect-error
baseFilters: variable.state.baseFilters,
filters: variable.state.filters,
});
} else {
throw new Error('Unsupported variable type');
}

@ -11,8 +11,9 @@ import {
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { config } from '@grafana/runtime';
import {
AdHocFilterSet,
AdHocFiltersVariable,
behaviors,
ConstantVariable,
CustomVariable,
DataSourceVariable,
QueryVariable,
@ -120,11 +121,11 @@ describe('transformSaveModelToScene', () => {
expect(scene.state?.$timeRange?.state.timeZone).toEqual('America/New_York');
expect(scene.state?.$timeRange?.state.weekStart).toEqual('saturday');
expect(scene.state?.$variables?.state.variables).toHaveLength(1);
expect(scene.state?.$variables?.state.variables).toHaveLength(2);
expect(scene.state?.$variables?.getByName('constant')).toBeInstanceOf(ConstantVariable);
expect(scene.state?.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable);
expect(dashboardControls).toBeDefined();
expect(dashboardControls).toBeInstanceOf(DashboardControls);
expect(dashboardControls.state.variableControls[1]).toBeInstanceOf(AdHocFilterSet);
expect((dashboardControls.state.variableControls[1] as AdHocFilterSet).state.name).toBe('CoolFilters');
expect(dashboardControls.state.timeControls).toHaveLength(2);
expect(dashboardControls.state.timeControls[0]).toBeInstanceOf(SceneTimePicker);
expect(dashboardControls.state.timeControls[1]).toBeInstanceOf(SceneRefreshPicker);
@ -789,6 +790,60 @@ describe('transformSaveModelToScene', () => {
});
});
it('should migrate adhoc variable', () => {
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',
},
],
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',
});
});
it.each(['system'])('should throw for unsupported (yet) variables', (type) => {
const variable = {
name: 'query0',
@ -856,7 +911,7 @@ describe('transformSaveModelToScene', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf(
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf(
SceneDataLayerControls
);
@ -886,7 +941,7 @@ describe('transformSaveModelToScene', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf(
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf(
SceneDataLayerControls
);
@ -902,7 +957,7 @@ describe('transformSaveModelToScene', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[2]).toBeInstanceOf(
expect((scene.state.controls![0] as DashboardControls)!.state.variableControls[1]).toBeInstanceOf(
SceneDataLayerControls
);

@ -1,4 +1,4 @@
import { AdHocVariableModel, TypedVariableModel, VariableModel } from '@grafana/data';
import { TypedVariableModel } from '@grafana/data';
import { config } from '@grafana/runtime';
import {
VizPanel,
@ -24,9 +24,9 @@ import {
SceneDataLayers,
SceneDataLayerProvider,
SceneDataLayerControls,
AdHocFilterSet,
TextBoxVariable,
UserActionEvent,
AdHocFiltersVariable,
} from '@grafana/scenes';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking';
@ -173,24 +173,11 @@ function createRowFromPanelModel(row: PanelModel, content: SceneGridItemLike[]):
export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) {
let variables: SceneVariableSet | undefined = undefined;
let layers: SceneDataLayerProvider[] = [];
let filtersSets: AdHocFilterSet[] = [];
if (oldModel.templating?.list?.length) {
const variableObjects = oldModel.templating.list
.map((v) => {
try {
if (isAdhocVariable(v)) {
filtersSets.push(
new AdHocFilterSet({
name: v.name,
datasource: v.datasource,
filters: v.filters ?? [],
baseFilters: v.baseFilters ?? [],
})
);
return null;
}
return createSceneVariableFromVariableModel(v);
} catch (err) {
console.error(err);
@ -282,7 +269,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
: undefined,
controls: [
new DashboardControls({
variableControls: [new VariableValueSelectors({}), ...filtersSets, new SceneDataLayerControls()],
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
timeControls: [
new SceneTimePicker({}),
new SceneRefreshPicker({
@ -305,6 +292,18 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
name: variable.name,
label: variable.label,
};
if (variable.type === 'adhoc') {
return new AdHocFiltersVariable({
...commonProperties,
description: variable.description,
skipUrlSync: variable.skipUrlSync,
hide: variable.hide,
datasource: variable.datasource,
applyMode: 'auto',
filters: variable.filters ?? [],
baseFilters: variable.baseFilters ?? [],
});
}
if (variable.type === 'custom') {
return new CustomVariable({
...commonProperties,
@ -480,8 +479,6 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
});
}
const isAdhocVariable = (v: VariableModel): v is AdHocVariableModel => v.type === 'adhoc';
const getLimitedDescriptionReporter = () => {
const reportedPanels: string[] = [];

@ -11,7 +11,6 @@ import {
VizPanel,
SceneDataTransformer,
SceneVariableSet,
AdHocFilterSet,
LocalValueVariable,
SceneRefreshPicker,
} from '@grafana/scenes';
@ -101,17 +100,6 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
refreshIntervals = control.state.intervals;
}
}
const variableControls = state.controls[0].state.variableControls;
for (const control of variableControls) {
if (control instanceof AdHocFilterSet) {
variables.push({
name: control.state.name!,
type: 'adhoc',
datasource: control.state.datasource,
});
}
}
}
if (state.$behaviors) {

@ -10,7 +10,14 @@ import {
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils, setRunRequest } from '@grafana/runtime';
import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
import {
SceneVariableSet,
CustomVariable,
SceneGridItem,
SceneGridLayout,
VizPanel,
AdHocFiltersVariable,
} from '@grafana/scenes';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
@ -97,11 +104,16 @@ describe('VariablesEditView', () => {
query: 'test3, test4, $customVar',
value: 'test3',
},
{
type: 'adhoc',
name: 'adhoc',
},
];
const variables = variableView.getVariables();
expect(variables).toHaveLength(2);
expect(variables).toHaveLength(3);
expect(variables[0].state).toMatchObject(expectedVariables[0]);
expect(variables[1].state).toMatchObject(expectedVariables[1]);
expect(variables[2].state).toMatchObject(expectedVariables[2]);
});
});
@ -117,7 +129,7 @@ describe('VariablesEditView', () => {
const variables = variableView.getVariables();
const variable = variables[0];
variableView.onDuplicated(variable.state.name);
expect(variableView.getVariables()).toHaveLength(3);
expect(variableView.getVariables()).toHaveLength(4);
expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar');
});
@ -125,7 +137,7 @@ describe('VariablesEditView', () => {
const variableIdentifier = 'customVar';
variableView.onDuplicated(variableIdentifier);
variableView.onDuplicated(variableIdentifier);
expect(variableView.getVariables()).toHaveLength(4);
expect(variableView.getVariables()).toHaveLength(5);
expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar_1');
expect(variableView.getVariables()[2].state.name).toBe('copy_of_customVar');
});
@ -133,7 +145,7 @@ describe('VariablesEditView', () => {
it('should delete a variable', () => {
const variableIdentifier = 'customVar';
variableView.onDelete(variableIdentifier);
expect(variableView.getVariables()).toHaveLength(1);
expect(variableView.getVariables()).toHaveLength(2);
expect(variableView.getVariables()[0].state.name).toBe('customVar2');
});
@ -147,7 +159,7 @@ describe('VariablesEditView', () => {
it('should keep the same order of variables with invalid indexes', () => {
const fromIndex = 0;
const toIndex = 2;
const toIndex = 3;
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
@ -163,11 +175,11 @@ describe('VariablesEditView', () => {
const previousVariable = variableView.getVariables()[1] as CustomVariable;
variableView.onEdit('customVar2');
variableView.onTypeChange('constant');
expect(variableView.getVariables()).toHaveLength(2);
variableView.onTypeChange('adhoc');
expect(variableView.getVariables()).toHaveLength(3);
const variable = variableView.getVariables()[1];
expect(variable).not.toBe(previousVariable);
expect(variable.state.type).toBe('constant');
expect(variable.state.type).toBe('adhoc');
// Values to be kept between the old and new variable
expect(variable.state.name).toEqual(previousVariable.state.name);
@ -184,9 +196,9 @@ describe('VariablesEditView', () => {
it('should add default new query variable when onAdd is called', () => {
variableView.onAdd();
expect(variableView.getVariables()).toHaveLength(3);
expect(variableView.getVariables()[2].state.name).toBe('query0');
expect(variableView.getVariables()[2].state.type).toBe('query');
expect(variableView.getVariables()).toHaveLength(4);
expect(variableView.getVariables()[3].state.name).toBe('query0');
expect(variableView.getVariables()[3].state.type).toBe('query');
});
afterEach(() => {
@ -266,6 +278,17 @@ async function buildTestScene() {
value: '$customVar',
text: '$customVar',
}),
new AdHocFiltersVariable({
type: 'adhoc',
name: 'adhoc',
filters: [
{
key: 'test',
operator: '=',
value: 'testValue',
},
],
}),
],
}),
body: new SceneGridLayout({

@ -1,14 +1,7 @@
import React from 'react';
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import {
SceneComponentProps,
SceneObjectBase,
SceneVariable,
SceneVariables,
sceneGraph,
AdHocFilterSet,
} from '@grafana/scenes';
import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
import { DashboardScene } from '../scene/DashboardScene';
@ -46,7 +39,7 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
return variables.findIndex((variable) => variable.state.name === identifier);
};
private replaceEditVariable = (newVariable: SceneVariable | AdHocFilterSet) => {
private replaceEditVariable = (newVariable: SceneVariable) => {
// Find the index of the variable to be deleted
const variableIndex = this.state.editIndex ?? -1;
const { variables } = this.getVariableSet().state;
@ -58,18 +51,10 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
return;
}
if (newVariable instanceof AdHocFilterSet) {
// TODO: Update controls in adding this fiter set to the dashboard
} else {
const updatedVariables = [
...variables.slice(0, variableIndex),
newVariable,
...variables.slice(variableIndex + 1),
];
// Update the state or the variables array
this.getVariableSet().setState({ variables: updatedVariables });
}
const updatedVariables = [...variables.slice(0, variableIndex), newVariable, ...variables.slice(variableIndex + 1)];
// Update the state or the variables array
this.getVariableSet().setState({ variables: updatedVariables });
};
public onDelete = (identifier: string) => {
@ -158,12 +143,9 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
const variableIndex = variables.length;
//add the new variable to the end of the array
const defaultNewVariable = getVariableDefault(variables);
if (defaultNewVariable instanceof AdHocFilterSet) {
// TODO: Update controls in adding this fiter set to the dashboard
} else {
this.getVariableSet().setState({ variables: [...this.getVariables(), defaultNewVariable] });
this.setState({ editIndex: variableIndex });
}
this.getVariableSet().setState({ variables: [...this.getVariables(), defaultNewVariable] });
this.setState({ editIndex: variableIndex });
};
public onTypeChange = (type: EditableVariableType) => {

@ -0,0 +1,75 @@
import { act, render } 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';
const defaultDatasource = mockDataSource({
name: 'Default Test Data Source',
uid: 'test-ds',
type: 'test',
});
const promDatasource = mockDataSource({
name: 'Prometheus',
uid: 'prometheus',
type: 'prometheus',
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
getDataSourceSrv: () => ({
get: async () => defaultDatasource,
getList: () => [defaultDatasource, promDatasource],
getInstanceSettings: () => ({ ...defaultDatasource }),
}),
}));
describe('AdHocVariableForm', () => {
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 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 () => {
const onDataSourceChange = jest.fn();
const { renderer, user } = await setup({
datasource: defaultDatasource,
onDataSourceChange,
infoText: 'Test Info',
});
// Simulate changing the data source
await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2));
await user.click(renderer.getByText(/prom/i));
expect(onDataSourceChange).toHaveBeenCalledTimes(1);
expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined);
});
});
async function setup(props?: React.ComponentProps<typeof AdHocVariableForm>) {
return {
renderer: await act(() => render(<AdHocVariableForm onDataSourceChange={jest.fn()} {...props} />)),
user: userEvent.setup(),
};
}

@ -0,0 +1,34 @@
import React from 'react';
import { DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourceRef } from '@grafana/schema';
import { Alert, Field } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { VariableLegend } from './VariableLegend';
interface AdHocVariableFormProps {
datasource?: DataSourceRef;
onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void;
infoText?: string;
}
export function AdHocVariableForm({ datasource, infoText, onDataSourceChange }: AdHocVariableFormProps) {
return (
<>
<VariableLegend>Ad-hoc options</VariableLegend>
<Field label="Data source" htmlFor="data-source-picker">
<DataSourcePicker current={datasource} onChange={onDataSourceChange} width={30} variables={true} noDefault />
</Field>
{infoText ? (
<Alert
title={infoText}
severity="info"
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText}
/>
) : null}
</>
);
}

@ -0,0 +1,122 @@
import { render, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { of } from 'rxjs';
import {
FieldType,
LoadingState,
PanelData,
VariableSupportType,
getDefaultTimeRange,
toDataFrame,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { setRunRequest } from '@grafana/runtime/src';
import { AdHocFiltersVariable } from '@grafana/scenes';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
import { AdHocFiltersVariableEditor } from './AdHocFiltersVariableEditor';
const defaultDatasource = mockDataSource({
name: 'Default Test Data Source',
uid: 'test-ds',
type: 'test',
});
const promDatasource = mockDataSource({
name: 'Prometheus',
uid: 'prometheus',
type: 'prometheus',
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
getDataSourceSrv: () => ({
get: async () => ({
...defaultDatasource,
variables: {
getType: () => VariableSupportType.Custom,
query: jest.fn(),
editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
},
}),
getList: () => [defaultDatasource, promDatasource],
getInstanceSettings: () => ({ ...defaultDatasource }),
}),
}));
const runRequestMock = jest.fn().mockReturnValue(
of<PanelData>({
state: LoadingState.Done,
series: [
toDataFrame({
fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
}),
],
timeRange: getDefaultTimeRange(),
})
);
setRunRequest(runRequestMock);
describe('AdHocFiltersVariableEditor', () => {
it('renders AdHocVariableForm with correct props', async () => {
const { renderer } = await setup();
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('This data source does not support ad hoc filters yet.');
});
it('should update the variable data source when data source picker is changed', async () => {
const { renderer, variable, user } = await setup();
// Simulate changing the data source
await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2));
await user.click(renderer.getByText(/prom/i));
expect(variable.state.datasource).toEqual({ uid: 'prometheus', type: 'prometheus' });
});
});
async function setup(props?: React.ComponentProps<typeof AdHocFiltersVariableEditor>) {
const onRunQuery = jest.fn();
const variable = new AdHocFiltersVariable({
name: 'adhocVariable',
type: 'adhoc',
label: 'Ad hoc filters',
description: 'Ad hoc filters are applied automatically to all queries that target this data source',
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
filters: [
{
key: 'test',
operator: '=',
value: 'testValue',
},
],
baseFilters: [
{
key: 'baseTest',
operator: '=',
value: 'baseTestValue',
},
],
});
return {
renderer: await act(() =>
render(<AdHocFiltersVariableEditor variable={variable} onRunQuery={onRunQuery} {...props} />)
),
variable,
user: userEvent.setup(),
mocks: { onRunQuery },
};
}

@ -1,12 +1,40 @@
import React from 'react';
import { useAsync } from 'react-use';
import { DataSourceInstanceSettings } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { AdHocFiltersVariable } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { AdHocVariableForm } from '../components/AdHocVariableForm';
interface AdHocFiltersVariableEditorProps {
variable: AdHocFiltersVariable;
onChange: (variable: AdHocFiltersVariable) => void;
onRunQuery: (variable: AdHocFiltersVariable) => void;
}
export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) {
return <div>AdHocFiltersVariableEditor</div>;
const { variable } = props;
const datasourceRef = variable.useState().datasource ?? undefined;
const { value: datasourceSettings } = useAsync(async () => {
return await getDataSourceSrv().get(datasourceRef);
}, [datasourceRef]);
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.';
const onDataSourceChange = (ds: DataSourceInstanceSettings) => {
const dsRef: DataSourceRef = {
uid: ds.uid,
type: ds.type,
};
variable.setState({
datasource: dsRef,
});
};
return <AdHocVariableForm datasource={datasourceRef} infoText={message} onDataSourceChange={onDataSourceChange} />;
}

@ -172,10 +172,10 @@ describe('getVariableScene', () => {
['adhoc', AdHocFiltersVariable],
['groupby', GroupByVariable],
['textbox', TextBoxVariable],
])('should return the scene variable instance for the given editable variable type', () => {
])('should return the scene variable instance for the given editable variable type', (type, instanceType) => {
const initialState = { name: 'MyVariable' };
const sceneVariable = getVariableScene('custom', initialState);
expect(sceneVariable).toBeInstanceOf(CustomVariable);
const sceneVariable = getVariableScene(type as EditableVariableType, initialState);
expect(sceneVariable).toBeInstanceOf(instanceType);
expect(sceneVariable.state.name).toBe(initialState.name);
});
});

@ -9,10 +9,10 @@ import {
IntervalVariable,
TextBoxVariable,
QueryVariable,
AdHocFilterSet,
GroupByVariable,
SceneVariable,
MultiValueVariable,
AdHocFiltersVariable,
SceneVariableState,
} from '@grafana/scenes';
import { VariableType } from '@grafana/schema';
@ -124,8 +124,7 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo
case 'datasource':
return new DataSourceVariable(initialState);
case 'adhoc':
// TODO: Initialize properly AdHocFilterSet with initialState
return new AdHocFilterSet({ name: initialState.name });
return new AdHocFiltersVariable(initialState);
case 'groupby':
return new GroupByVariable(initialState);
case 'textbox':

@ -10,7 +10,7 @@ export function getVariablesCompatibility(sceneObject: SceneObject): TypedVariab
// Sadly templateSrv.getVariables returns TypedVariableModel but sceneVariablesSetToVariables return persisted schema model
// They look close to identical (differ in what is optional in some places).
// The way templateSrv.getVariables is used it should not matter. it is mostly used to get names of all variables (for query editors).
// So type and name are important. Maybe some external data sourcess also check current value so that is also important.
// So type and name are important. Maybe some external data sources also check current value so that is also important.
// @ts-expect-error
return legacyModels;
}

@ -28,9 +28,9 @@ export class AddToFiltersGraphAction extends SceneObjectBase<AddToFiltersGraphAc
const labelName = Object.keys(labels)[0];
variable.state.set.setState({
variable.setState({
filters: [
...variable.state.set.state.filters,
...variable.state.filters,
{
key: labelName,
operator: '=',

@ -11,7 +11,7 @@ export function getLabelOptions(scenObject: SceneObject, variable: QueryVariable
return [];
}
const filters = labelFilters.state.set.state.filters;
const filters = labelFilters.state.filters;
for (const option of variable.getOptionsForSelect()) {
const filterExists = filters.find((f) => f.key === option.value);

@ -137,7 +137,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
// Add metric to adhoc filters baseFilter
const filterVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
if (filterVar instanceof AdHocFiltersVariable) {
filterVar.state.set.setState({
filterVar.setState({
baseFilters: getBaseFiltersForMetric(evt.payload),
});
}
@ -208,7 +208,7 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad
value: initialDS,
pluginId: metric === LOGS_METRIC ? 'loki' : 'prometheus',
}),
AdHocFiltersVariable.create({
new AdHocFiltersVariable({
name: VAR_FILTERS,
datasource: trailDS,
layout: 'vertical',

@ -23,7 +23,7 @@ export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
return null;
}
const filters = filtersVariable.state.set.state.filters;
const filters = filtersVariable.state.filters;
const dsValue = getDataSource(trail);
return (

@ -2,6 +2,13 @@ import { BOOKMARKED_TRAILS_KEY, RECENT_TRAILS_KEY } from '../shared';
import { SerializedTrail, getTrailStore } from './TrailStore';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => ({
getAdhocFilters: jest.fn().mockReturnValue([{ key: 'origKey', operator: '=', value: '' }]),
}),
}));
describe('TrailStore', () => {
beforeAll(() => {
let localStore: Record<string, string> = {};

@ -1,90 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { ComponentProps } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { adHocBuilder } from '../shared/testing/builders';
import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor';
const promDsMock = mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
});
const lokiDsMock = mockDataSource({
name: 'Loki',
type: DataSourceType.Loki,
});
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
return {
getDataSourceSrv: () => ({
get: () => {
return Promise.resolve(promDsMock);
},
getList: () => [promDsMock, lokiDsMock],
getInstanceSettings: (v: string) => {
if (v === 'Prometheus') {
return promDsMock;
}
return lokiDsMock;
},
}),
};
});
const props = {
extended: {
dataSources: [
{ text: 'Prometheus', value: null }, // default datasource
{ text: 'Loki', value: { type: 'loki-ds', uid: 'abc' } },
],
} as ComponentProps<typeof AdHocVariableEditor>['extended'],
variable: adHocBuilder().withId('adhoc').withRootStateKey('key').withName('adhoc').build(),
onPropChange: jest.fn(),
// connected actions
initAdHocVariableEditor: jest.fn(),
changeVariableDatasource: jest.fn(),
};
describe('AdHocVariableEditor', () => {
beforeEach(() => {
props.changeVariableDatasource.mockReset();
});
it('has a datasource select menu', async () => {
render(<AdHocVariableEditor {...props} />);
expect(await screen.getByTestId(selectors.components.DataSourcePicker.container)).toBeInTheDocument();
});
it('calls the callback when changing the datasource', async () => {
render(<AdHocVariableEditor {...props} />);
const selectEl = screen
.getByTestId(selectors.components.DataSourcePicker.container)
.getElementsByTagName('input')[0];
await userEvent.click(selectEl);
await userEvent.click(screen.getByText('Loki'));
expect(props.changeVariableDatasource).toBeCalledWith(
{ type: 'adhoc', id: 'adhoc', rootStateKey: 'key' },
{ type: 'loki', uid: 'mock-ds-3' }
);
});
it('renders informational text', () => {
const extended = {
...props.extended,
infoText: "Here's a message that should help you",
};
render(<AdHocVariableEditor {...props} extended={extended} />);
const alert = screen.getByText("Here's a message that should help you");
expect(alert).toBeInTheDocument();
});
});

@ -2,11 +2,9 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { Alert, Field } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { AdHocVariableForm } from 'app/features/dashboard-scene/settings/variables/components/AdHocVariableForm';
import { StoreState } from 'app/types';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { initialVariableEditorState } from '../editor/reducer';
import { getAdhocVariableEditorState } from '../editor/selectors';
import { VariableEditorProps } from '../editor/types';
@ -58,23 +56,13 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
render() {
const { variable, extended } = this.props;
const infoText = extended?.infoText ?? null;
return (
<>
<VariableLegend>Ad-hoc options</VariableLegend>
<Field label="Data source" htmlFor="data-source-picker">
<DataSourcePicker
current={variable.datasource}
onChange={this.onDatasourceChanged}
width={30}
variables={true}
noDefault
/>
</Field>
{infoText ? <Alert title={infoText} severity="info" /> : null}
</>
<AdHocVariableForm
datasource={variable.datasource ?? undefined}
onDataSourceChange={this.onDatasourceChanged}
infoText={extended?.infoText}
/>
);
}
}

@ -4020,9 +4020,9 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes@npm:^2.6.5":
version: 2.6.5
resolution: "@grafana/scenes@npm:2.6.5"
"@grafana/scenes@npm:^3.2.1":
version: 3.2.1
resolution: "@grafana/scenes@npm:3.2.1"
dependencies:
"@grafana/e2e-selectors": "npm:10.0.2"
react-grid-layout: "npm:1.3.4"
@ -4034,7 +4034,7 @@ __metadata:
"@grafana/runtime": 10.0.3
"@grafana/schema": 10.0.3
"@grafana/ui": 10.0.3
checksum: 10/68fe91a5a0c8f80b679126f3525b74b29ce3f9ad92bc558eaaf39693235137348b90796c2b02aa7c1c7929586e60df6024e139993a416ef37d4c875e548dc855
checksum: 10/5e93c0dcdfbd7cfed977d650fb0744c9b8107b1c92af2ae6d6e9f2f61bb9ba7f3cb5ad394fa3389aaef0d892a18c6a12fedf5454a4210bdcc6797a272ddbc625
languageName: node
linkType: hard
@ -18026,7 +18026,7 @@ __metadata:
"@grafana/o11y-ds-frontend": "workspace:*"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:^2.6.5"
"@grafana/scenes": "npm:^3.2.1"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/tsconfig": "npm:^1.3.0-rc1"

Loading…
Cancel
Save