grafana_bak/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx
2025-04-01 10:38:02 +09:00

363 lines
12 KiB
TypeScript

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<PanelData>({
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 };
}