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/panel-edit/PanelEditor.test.ts

359 lines
11 KiB

import { of } from 'rxjs';
import { DataQueryRequest, DataSourceApi, LoadingState, PanelPlugin } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setDataSourceSrv } from '@grafana/runtime';
import {
CancelActivationHandler,
CustomVariable,
SceneDataTransformer,
sceneGraph,
SceneGridLayout,
SceneQueryRunner,
SceneTimeRange,
SceneVariableSet,
VizPanel,
} from '@grafana/scenes';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import * as libAPI from 'app/features/library-panels/state/api';
import { DashboardGridItem } from '../scene/DashboardGridItem';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { activateFullSceneTree } from '../utils/test-utils';
import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils';
import { buildPanelEditScene } from './PanelEditor';
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
return of({
state: LoadingState.Loading,
series: [],
timeRange: request.range,
});
});
let pluginPromise: Promise<PanelPlugin> | undefined;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
},
getPluginImportUtils: () => ({
getPanelPluginFromCache: jest.fn(() => undefined),
importPanelPlugin: () => pluginPromise,
}),
config: {
...jest.requireActual('@grafana/runtime').config,
panels: {
text: {
skipDataQuery: true,
},
timeseries: {
skipDataQuery: false,
},
},
},
}));
const dataSources = {
ds1: mockDataSource({
uid: 'ds1',
type: DataSourceType.Prometheus,
}),
};
setDataSourceSrv(new MockDataSourceSrv(dataSources));
let deactivate: CancelActivationHandler | undefined;
describe('PanelEditor', () => {
afterEach(() => {
if (deactivate) {
deactivate();
deactivate = undefined;
}
});
describe('When initializing', () => {
it('should wait for panel plugin to load', async () => {
const { panelEditor, panel, pluginResolve, dashboard } = await setup({ skipWait: true });
expect(panel.state.options).toEqual({});
expect(panelEditor.state.isInitializing).toBe(true);
const pluginToLoad = getPanelPlugin({ id: 'text' }).setPanelOptions((build) => {
build.addBooleanSwitch({
path: 'showHeader',
name: 'Show header',
defaultValue: true,
});
});
pluginResolve(pluginToLoad);
await new Promise((r) => setTimeout(r, 1));
expect(panelEditor.state.isInitializing).toBe(false);
expect(panel.state.options).toEqual({ showHeader: true });
panel.onOptionsChange({ showHeader: false });
panelEditor.onDiscard();
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
expect(discardedPanel.state.options).toEqual({ showHeader: true });
});
});
describe('When discarding', () => {
it('should discard changes revert all changes', async () => {
const { panelEditor, panel, dashboard } = await setup();
panel.setState({ title: 'changed title' });
panelEditor.onDiscard();
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
expect(discardedPanel.state.title).toBe('original title');
});
it('should discard a newly added panel', async () => {
const { panelEditor, dashboard } = await setup({ isNewPanel: true });
panelEditor.onDiscard();
const panels = dashboard.state.body.getVizPanels();
expect(panels.length).toBe(0);
});
it('should discard query runner changes', async () => {
const { panelEditor, panel, dashboard } = await setup({});
const queryRunner = getQueryRunnerFor(panel);
queryRunner?.setState({ maxDataPoints: 123, queries: [{ refId: 'A' }, { refId: 'B' }] });
panelEditor.onDiscard();
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
const restoredQueryRunner = getQueryRunnerFor(discardedPanel);
expect(restoredQueryRunner?.state.maxDataPoints).toBe(500);
expect(restoredQueryRunner?.state.queries.length).toBe(1);
});
});
describe('When changes are made', () => {
it('Should set state to dirty', async () => {
const { panelEditor, panel } = await setup({});
expect(panelEditor.state.isDirty).toBe(undefined);
panel.setState({ title: 'changed title' });
expect(panelEditor.state.isDirty).toBe(true);
});
it('Should reset dirty and orginal state when dashboard is saved', async () => {
const { panelEditor, panel } = await setup({});
expect(panelEditor.state.isDirty).toBe(undefined);
panel.setState({ title: 'changed title' });
panelEditor.dashboardSaved();
expect(panelEditor.state.isDirty).toBe(false);
panel.setState({ title: 'changed title 2' });
expect(panelEditor.state.isDirty).toBe(true);
// Change back to already saved state
panel.setState({ title: 'changed title' });
expect(panelEditor.state.isDirty).toBe(false);
});
});
describe('When opening a repeated panel', () => {
it('Should default to the first variable value if panel is repeated', async () => {
const { panel } = await setup({ repeatByVariable: 'server' });
const variable = sceneGraph.lookupVariable('server', panel);
expect(variable?.getValue()).toBe('A');
});
});
describe('Handling library panels', () => {
it('should call the api with the updated panel', async () => {
pluginPromise = Promise.resolve(getPanelPlugin({ id: 'text', skipDataQuery: true }));
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text' });
const libraryPanelModel = {
title: 'title',
uid: 'uid',
name: 'libraryPanelName',
model: vizPanelToPanel(panel),
type: 'panel',
version: 1,
};
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: true,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
_loadedPanel: libraryPanelModel,
});
panel.setState({ $behaviors: [libPanelBehavior] });
const gridItem = new DashboardGridItem({ body: panel });
const editScene = buildPanelEditScene(panel);
const scene = new DashboardScene({
editPanel: editScene,
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
isEditing: true,
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [gridItem],
}),
}),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
panel.setState({ title: 'changed title' });
libPanelBehavior.setState({ name: 'changed name' });
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
libPanelBehavior.setPanelFromLibPanel(updatedPanel);
});
editScene.onConfirmSaveLibraryPanel();
await new Promise(process.nextTick);
// Wait for mock api to return and update the library panel
expect(libPanelBehavior.state._loadedPanel?.version).toBe(2);
expect(libPanelBehavior.state.name).toBe('changed name');
expect(panel.state.title).toBe('changed title');
expect((gridItem.state.body as VizPanel).state.title).toBe('changed title');
});
it('unlinks library panel', () => {
const libraryPanelModel = {
title: 'title',
uid: 'uid',
name: 'libraryPanelName',
model: {
title: 'title',
type: 'text',
},
type: 'panel',
version: 1,
};
const libPanelBehavior = new LibraryPanelBehavior({
isLoaded: true,
uid: libraryPanelModel.uid,
name: libraryPanelModel.name,
_loadedPanel: libraryPanelModel,
});
// Just adding an extra stateless behavior to verify unlinking does not remvoe it
const otherBehavior = jest.fn();
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text', $behaviors: [libPanelBehavior, otherBehavior] });
const editScene = buildPanelEditScene(panel);
editScene.onConfirmUnlinkLibraryPanel();
expect(panel.state.$behaviors?.length).toBe(1);
expect(panel.state.$behaviors![0]).toBe(otherBehavior);
});
});
describe('PanelDataPane', () => {
it('should not exist if panel is skipDataQuery', async () => {
const { panelEditor, panel } = await setup({ pluginSkipDataQuery: true });
expect(panelEditor.state.dataPane).toBeUndefined();
expect(panel.state.$data).toBeUndefined();
});
it('should exist if panel is supporting querying', async () => {
const { panelEditor, panel } = await setup({ pluginSkipDataQuery: false });
expect(panelEditor.state.dataPane).toBeDefined();
expect(panel.state.$data).toBeDefined();
});
});
});
interface SetupOptions {
isNewPanel?: boolean;
pluginSkipDataQuery?: boolean;
repeatByVariable?: string;
skipWait?: boolean;
pluginLoadTime?: number;
}
async function setup(options: SetupOptions = {}) {
const pluginToLoad = getPanelPlugin({ id: 'text', skipDataQuery: options.pluginSkipDataQuery });
let pluginResolve = (plugin: PanelPlugin) => {};
pluginPromise = new Promise<PanelPlugin>((resolve) => {
pluginResolve = resolve;
});
const panel = new VizPanel({
key: 'panel-1',
pluginId: 'text',
title: 'original title',
$data: new SceneDataTransformer({
transformations: [],
$data: new SceneQueryRunner({
queries: [{ refId: 'A' }],
maxDataPoints: 500,
datasource: { uid: 'ds1' },
}),
}),
});
const gridItem = new DashboardGridItem({ body: panel, variableName: options.repeatByVariable });
const panelEditor = buildPanelEditScene(panel, options.isNewPanel);
const dashboard = new DashboardScene({
editPanel: panelEditor,
isEditing: true,
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
$variables: new SceneVariableSet({
variables: [
new CustomVariable({
name: 'server',
query: 'A,B,C',
isMulti: true,
value: ['A', 'B', 'C'],
text: ['A', 'B', 'C'],
}),
],
}),
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [gridItem],
}),
}),
});
panelEditor.debounceSaveModelDiff = false;
deactivate = activateFullSceneTree(dashboard);
if (!options.skipWait) {
//console.log('pluginResolve(pluginToLoad)');
pluginResolve(pluginToLoad);
await new Promise((r) => setTimeout(r, 1));
}
return { dashboard, panel, gridItem, panelEditor, pluginResolve };
}