The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx

362 lines
12 KiB

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 };
}