import { of } from 'rxjs'; import { FieldType, LoadingState, PanelData, VariableSupportType, getDefaultTimeRange, toDataFrame, } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneVariableSet, CustomVariable, VizPanel, AdHocFiltersVariable, SceneVariableState, SceneTimeRange, } from '@grafana/scenes'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { activateFullSceneTree } from '../utils/test-utils'; import { VariablesEditView } from './VariablesEditView'; setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), getPanelPluginFromCache: (id: string) => undefined, }); const defaultDatasource = mockDataSource({ name: 'Default Test Data Source', type: 'test', }); const promDatasource = mockDataSource({ name: '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({ state: LoadingState.Done, series: [ toDataFrame({ fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }], }), ], timeRange: getDefaultTimeRange(), }) ); setRunRequest(runRequestMock); describe('VariablesEditView', () => { describe('Dashboard Variables state', () => { let dashboard: DashboardScene; let variableView: VariablesEditView; beforeEach(async () => { const result = await buildTestScene(); dashboard = result.dashboard; variableView = result.variableView; }); it('should return the correct urlKey', () => { expect(variableView.getUrlKey()).toBe('variables'); }); it('should return the dashboard', () => { expect(variableView.getDashboard()).toBe(dashboard); }); it('should return the list of variables', () => { const expectedVariables = [ { type: 'custom', name: 'customVar', query: 'test, test2', value: 'test', }, { type: 'custom', name: 'customVar2', query: 'test3, test4, $customVar', value: 'test3', }, { type: 'adhoc', name: 'adhoc', }, ]; const variables = variableView.getVariables(); 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]); }); }); describe('Dashboard Variables actions', () => { let variableView: VariablesEditView; beforeEach(async () => { const result = await buildTestScene(); variableView = result.variableView; }); it('should duplicate a variable', () => { const variables = variableView.getVariables(); const variable = variables[0]; variableView.onDuplicated(variable.state.name); expect(variableView.getVariables()).toHaveLength(4); expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar'); }); it('should handle name when duplicating a variable twice', () => { const variableIdentifier = 'customVar'; variableView.onDuplicated(variableIdentifier); variableView.onDuplicated(variableIdentifier); 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'); }); it('should delete a variable', () => { const variableIdentifier = 'customVar'; variableView.onEdit(variableIdentifier); expect(variableView.state.editIndex).toBe(0); variableView.onDelete(variableIdentifier); expect(variableView.getVariables()).toHaveLength(2); expect(variableView.getVariables()[0].state.name).toBe('customVar2'); expect(variableView.state.editIndex).toBeUndefined(); }); it('should change order of variables', () => { const fromIndex = 0; // customVar is first const toIndex = 1; variableView.onOrderChanged(fromIndex, toIndex); expect(variableView.getVariables()[0].state.name).toBe('customVar2'); expect(variableView.getVariables()[1].state.name).toBe('customVar'); }); it('should keep the same order of variables with invalid indexes', () => { const fromIndex = 0; const toIndex = 3; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); variableView.onOrderChanged(fromIndex, toIndex); expect(errorSpy).toHaveBeenCalledTimes(1); expect(variableView.getVariables()[0].state.name).toBe('customVar'); expect(variableView.getVariables()[1].state.name).toBe('customVar2'); errorSpy.mockRestore(); }); it('should change the variable type creating a new variable object', () => { const previousVariable = variableView.getVariables()[1] as CustomVariable; variableView.onEdit('customVar2'); variableView.onTypeChange('adhoc'); expect(variableView.getVariables()).toHaveLength(3); const variable = variableView.getVariables()[1]; expect(variable).not.toBe(previousVariable); expect(variable.state.type).toBe('adhoc'); // Values to be kept between the old and new variable expect(variable.state.name).toEqual(previousVariable.state.name); expect(variable.state.label).toEqual(previousVariable.state.label); }); it('should reset editing variable when going back', () => { variableView.onEdit('customVar2'); expect(variableView.state.editIndex).toBe(1); variableView.onGoBack(); expect(variableView.state.editIndex).toBeUndefined(); }); it('should add default new query variable when onAdd is called', () => { variableView.onAdd(); expect(variableView.getVariables()).toHaveLength(4); expect(variableView.getVariables()[3].state.name).toBe('query0'); expect(variableView.getVariables()[3].state.type).toBe('query'); }); afterEach(() => { jest.clearAllMocks(); }); }); describe('Variables name validation', () => { let variableView: VariablesEditView; let variable1: SceneVariableState; let variable2: SceneVariableState; beforeAll(async () => { const result = await buildTestScene(); variableView = result.variableView; const variables = variableView.getVariables(); variable1 = variables[0].state; variable2 = variables[1].state; }); it('should not return error on same name and key', () => { expect(variableView.onValidateVariableName(variable1.name, variable1.key)[0]).toBe(false); }); it('should not return error if name is unique', () => { expect(variableView.onValidateVariableName('unique_variable_name', variable1.key)[0]).toBe(false); }); it('should return error if global variable name is used', () => { expect(variableView.onValidateVariableName('__', variable1.key)[0]).toBe(true); }); it('should not return error if global variable name is used not at the beginning ', () => { expect(variableView.onValidateVariableName('test__', variable1.key)[0]).toBe(false); }); it('should return error if name is empty', () => { expect(variableView.onValidateVariableName('', variable1.key)[0]).toBe(true); }); it('should return error if non word characters are used', () => { expect(variableView.onValidateVariableName('-', variable1.key)[0]).toBe(true); }); it('should return error if variable name is taken', () => { expect(variableView.onValidateVariableName(variable2.name, variable1.key)[0]).toBe(true); }); }); describe('Dashboard Variables dependencies', () => { let variableView: VariablesEditView; let dashboard: DashboardScene; beforeEach(async () => { const result = await buildTestScene(); variableView = result.variableView; dashboard = result.dashboard; }); // FIXME: This is not working because the variable is replaced or it is not resolved yet it.skip('should keep dependencies between variables the type is changed so the variable is replaced', () => { // Uses function to avoid store reference to previous existing variables const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable; const getDependantVariable = () => variableView.getVariables()[1] as CustomVariable; expect(getSourceVariable().getValue()).toBe('test'); // Using getOptionsForSelect to get the interpolated values expect(getDependantVariable().getOptionsForSelect()[2].label).toBe('test'); variableView.onEdit(getSourceVariable().state.name); // Simulating changing the type and update the value variableView.onTypeChange('constant'); getSourceVariable().setState({ value: 'newValue' }); expect(getSourceVariable().getValue()).toBe('newValue'); expect(getDependantVariable().getOptionsForSelect()[2].label).toBe('newValue'); }); it('should keep dependencies with panels when the type is changed so the variable is replaced', async () => { // Uses function to avoid store reference to previous existing variables const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable; const getDependantPanel = () => dashboard.state.body.getVizPanels()[0]; expect(getSourceVariable().getValue()).toBe('test'); // Using description to get the interpolated value expect(getDependantPanel().getDescription()).toContain('Panel A depends on customVar with current value test'); variableView.onEdit(getSourceVariable().state.name); // Simulating changing the type and update the value variableView.onTypeChange('constant'); getSourceVariable().setState({ value: 'newValue' }); expect(getSourceVariable().getValue()).toBe('newValue'); expect(getDependantPanel().getDescription()).toContain('newValue'); }); }); }); async function buildTestScene() { const variableView = new VariablesEditView({}); const dashboard = new DashboardScene({ title: 'Dashboard with variables', uid: 'dash-variables', meta: { canEdit: true, }, $timeRange: new SceneTimeRange({}), $variables: new SceneVariableSet({ variables: [ new CustomVariable({ name: 'customVar', query: 'test, test2', value: 'test', text: 'test', }), new CustomVariable({ name: 'customVar2', query: 'test3, test4, $customVar', value: '$customVar', text: '$customVar', }), new AdHocFiltersVariable({ type: 'adhoc', name: 'adhoc', filters: [ { key: 'test', operator: '=', value: 'testValue', }, ], }), ], }), body: DefaultGridLayoutManager.fromVizPanels([ new VizPanel({ title: 'Panel A', description: 'Panel A depends on customVar with current value $customVar', key: 'panel-1', pluginId: 'table', }), ]), editview: variableView, }); activateFullSceneTree(dashboard); await new Promise((r) => setTimeout(r, 1)); dashboard.onEnterEditMode(); variableView.activate(); return { dashboard, variableView }; }