mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
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.
1105 lines
32 KiB
1105 lines
32 KiB
import { advanceTo } from 'jest-date-mock';
|
|
import { map, of } from 'rxjs';
|
|
|
|
import {
|
|
DataFrame,
|
|
DataQueryRequest,
|
|
DataSourceApi,
|
|
dateTime,
|
|
FieldType,
|
|
PanelData,
|
|
standardTransformersRegistry,
|
|
StandardVariableQuery,
|
|
toDataFrame,
|
|
VariableSupportType,
|
|
} from '@grafana/data';
|
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
|
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
|
|
import { MultiValueVariable, SceneGridLayout, SceneGridRow, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
|
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
|
import { PanelModel } from 'app/features/dashboard/state';
|
|
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
|
import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor';
|
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
|
|
|
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
|
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
|
import { DashboardScene } from '../scene/DashboardScene';
|
|
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
|
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
|
import { NEW_LINK } from '../settings/links/utils';
|
|
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
|
|
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
|
|
|
import { GRAFANA_DATASOURCE_REF } from './const';
|
|
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
|
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
|
|
import snapshotableDashboardJson from './testfiles/snapshotable_dashboard.json';
|
|
import snapshotableWithRowsDashboardJson from './testfiles/snapshotable_with_rows.json';
|
|
import {
|
|
buildGridItemForLibPanel,
|
|
buildGridItemForPanel,
|
|
transformSaveModelToScene,
|
|
} from './transformSaveModelToScene';
|
|
import {
|
|
gridItemToPanel,
|
|
gridRowToSaveModel,
|
|
panelRepeaterToPanels,
|
|
transformSceneToSaveModel,
|
|
trimDashboardForSnapshot,
|
|
} from './transformSceneToSaveModel';
|
|
|
|
standardTransformersRegistry.setInit(() => [reduceTransformRegistryItem]);
|
|
setPluginImportUtils({
|
|
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
|
getPanelPluginFromCache: (id: string) => undefined,
|
|
});
|
|
|
|
const AFrame = toDataFrame({
|
|
name: 'A',
|
|
fields: [
|
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
|
{ name: 'values', type: FieldType.number, values: [1, 2, 3] },
|
|
],
|
|
});
|
|
|
|
const BFrame = toDataFrame({
|
|
name: 'B',
|
|
fields: [
|
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
|
{ name: 'values', type: FieldType.number, values: [10, 20, 30] },
|
|
],
|
|
});
|
|
|
|
const CFrame = toDataFrame({
|
|
name: 'C',
|
|
fields: [
|
|
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000] },
|
|
{ name: 'values', type: FieldType.number, values: [100, 200, 300] },
|
|
],
|
|
});
|
|
|
|
const AnnoFrame = toDataFrame({
|
|
fields: [
|
|
{ name: 'time', values: [1, 2, 2, 5, 5] },
|
|
{ name: 'id', values: ['1', '2', '2', '5', '5'] },
|
|
{ name: 'text', values: ['t1', 't2', 't3', 't4', 't5'] },
|
|
],
|
|
});
|
|
|
|
const VariableQueryFrame = toDataFrame({
|
|
fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
|
|
});
|
|
|
|
const testSeries: Record<string, DataFrame> = {
|
|
A: AFrame,
|
|
B: BFrame,
|
|
C: CFrame,
|
|
Anno: AnnoFrame,
|
|
VariableQuery: VariableQueryFrame,
|
|
};
|
|
|
|
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
|
const result: PanelData = {
|
|
state: LoadingState.Loading,
|
|
series: [],
|
|
timeRange: request.range,
|
|
};
|
|
|
|
return of([]).pipe(
|
|
map(() => {
|
|
result.state = LoadingState.Done;
|
|
|
|
const refId = request.targets[0].refId;
|
|
result.series = [testSeries[refId]];
|
|
|
|
return result;
|
|
})
|
|
);
|
|
});
|
|
|
|
jest.mock('@grafana/runtime', () => ({
|
|
...jest.requireActual('@grafana/runtime'),
|
|
getDataSourceSrv: () => ({
|
|
get: () => ({
|
|
getRef: () => ({ type: 'mock-ds', uid: 'mock-uid' }),
|
|
variables: {
|
|
getType: () => VariableSupportType.Standard,
|
|
toDataQuery: (q: StandardVariableQuery) => q,
|
|
},
|
|
}),
|
|
}),
|
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
|
return runRequestMock(ds, request);
|
|
},
|
|
config: {
|
|
panels: {
|
|
text: { skipDataQuery: true },
|
|
},
|
|
featureToggles: {
|
|
dataTrails: false,
|
|
},
|
|
theme2: {
|
|
visualization: {
|
|
getColorByName: jest.fn().mockReturnValue('red'),
|
|
},
|
|
},
|
|
},
|
|
setPluginExtensionGetter: jest.fn(),
|
|
getPluginLinkExtensions: jest.fn(),
|
|
}));
|
|
|
|
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
|
|
|
|
jest.mock('@grafana/scenes', () => ({
|
|
...jest.requireActual('@grafana/scenes'),
|
|
sceneUtils: {
|
|
...jest.requireActual('@grafana/scenes').sceneUtils,
|
|
registerVariableMacro: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('transformSceneToSaveModel', () => {
|
|
beforeEach(() => {
|
|
getPluginLinkExtensionsMock.mockRestore();
|
|
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
|
});
|
|
|
|
describe('Given a simple scene with custom settings', () => {
|
|
it('Should transform back to persisted model', () => {
|
|
const dashboardWithCustomSettings = {
|
|
...dashboard_to_load1,
|
|
title: 'My custom title',
|
|
description: 'My custom description',
|
|
tags: ['tag1', 'tag2'],
|
|
timezone: 'America/New_York',
|
|
weekStart: 'monday',
|
|
graphTooltip: 1,
|
|
editable: false,
|
|
refresh: '5m',
|
|
timepicker: {
|
|
...dashboard_to_load1.timepicker,
|
|
refresh_intervals: ['5m', '15m', '30m', '1h'],
|
|
time_options: ['5m', '15m', '30m'],
|
|
hidden: true,
|
|
},
|
|
links: [{ ...NEW_LINK, title: 'Link 1' }],
|
|
};
|
|
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as any, meta: {} });
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
|
|
expect(saveModel).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('Given a simple scene with variables', () => {
|
|
it('Should transform back to persisted model', () => {
|
|
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
|
|
expect(saveModel).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
describe('Given a scene with rows', () => {
|
|
it('Should transform back to persisted model', () => {
|
|
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });
|
|
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
|
|
const row2: RowPanel = saveModel.panels![2] as RowPanel;
|
|
|
|
expect(row2.type).toBe('row');
|
|
expect(row2.repeat).toBe('server');
|
|
expect(saveModel).toMatchSnapshot();
|
|
});
|
|
|
|
it('Should remove repeated rows in save model', () => {
|
|
const scene = transformSaveModelToScene({ dashboard: repeatingRowsAndPanelsDashboardJson as any, meta: {} });
|
|
|
|
const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable;
|
|
variable.changeValueTo(['a', 'b', 'c']);
|
|
|
|
const grid = scene.state.body as SceneGridLayout;
|
|
const rowWithRepeat = grid.state.children[1] as SceneGridRow;
|
|
const rowRepeater = rowWithRepeat.state.$behaviors![0] as RowRepeaterBehavior;
|
|
|
|
// trigger row repeater
|
|
rowRepeater.performRepeat();
|
|
|
|
// Make sure the repeated rows have been added to runtime scene model
|
|
expect(grid.state.children.length).toBe(5);
|
|
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
const rows = saveModel.panels!.filter((p) => p.type === 'row');
|
|
// Verify the save model does not contain any repeated rows
|
|
expect(rows.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('Panel options', () => {
|
|
it('Given panel with time override', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({
|
|
timeFrom: '2h',
|
|
timeShift: '1d',
|
|
hideTimeOverride: true,
|
|
});
|
|
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
expect(saveModel.timeFrom).toBe('2h');
|
|
expect(saveModel.timeShift).toBe('1d');
|
|
expect(saveModel.hideTimeOverride).toBe(true);
|
|
});
|
|
|
|
it('transparent panel', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({ transparent: true });
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
|
|
expect(saveModel.transparent).toBe(true);
|
|
});
|
|
|
|
it('interval', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({ interval: '20m' });
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
|
|
expect(saveModel.interval).toBe('20m');
|
|
});
|
|
|
|
it('With angular options', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({});
|
|
const vizPanel = gridItem.state.body as VizPanel;
|
|
vizPanel.setState({
|
|
options: {
|
|
angularOptions: {
|
|
bars: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
expect(saveModel.options?.angularOptions).toBe(undefined);
|
|
expect((saveModel as any).bars).toBe(true);
|
|
});
|
|
|
|
it('Given panel with repeat', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({
|
|
title: '',
|
|
type: 'text-plugin-34',
|
|
gridPos: { x: 1, y: 2, w: 12, h: 8 },
|
|
repeat: 'server',
|
|
repeatDirection: 'v',
|
|
maxPerRow: 8,
|
|
});
|
|
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
expect(saveModel.repeat).toBe('server');
|
|
expect(saveModel.repeatDirection).toBe('v');
|
|
expect(saveModel.maxPerRow).toBe(8);
|
|
expect(saveModel.gridPos?.x).toBe(1);
|
|
expect(saveModel.gridPos?.y).toBe(2);
|
|
expect(saveModel.gridPos?.w).toBe(12);
|
|
expect(saveModel.gridPos?.h).toBe(8);
|
|
});
|
|
it('Given panel with links', () => {
|
|
const gridItem = buildGridItemFromPanelSchema({
|
|
title: '',
|
|
type: 'text-plugin-34',
|
|
gridPos: { x: 1, y: 2, w: 12, h: 8 },
|
|
links: [
|
|
// @ts-expect-error Panel link is wrongly typed as DashboardLink
|
|
{
|
|
title: 'Link 1',
|
|
url: 'http://some.test.link1',
|
|
},
|
|
// @ts-expect-error Panel link is wrongly typed as DashboardLink
|
|
{
|
|
targetBlank: true,
|
|
title: 'Link 2',
|
|
url: 'http://some.test.link2',
|
|
},
|
|
],
|
|
});
|
|
|
|
const saveModel = gridItemToPanel(gridItem);
|
|
expect(saveModel.links).toEqual([
|
|
{
|
|
title: 'Link 1',
|
|
url: 'http://some.test.link1',
|
|
},
|
|
{
|
|
targetBlank: true,
|
|
title: 'Link 2',
|
|
url: 'http://some.test.link2',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Library panels', () => {
|
|
it('given a library panel', () => {
|
|
// Not using buildGridItemFromPanelSchema since it strips options/fieldConfig
|
|
const libVizPanel = new LibraryVizPanel({
|
|
name: 'Some lib panel panel',
|
|
title: 'A panel',
|
|
uid: 'lib-panel-uid',
|
|
panelKey: 'lib-panel',
|
|
panel: new VizPanel({
|
|
key: 'panel-4',
|
|
title: 'Panel blahh blah',
|
|
fieldConfig: {
|
|
defaults: {},
|
|
overrides: [],
|
|
},
|
|
options: {
|
|
legend: {
|
|
calcs: [],
|
|
displayMode: 'list',
|
|
placement: 'bottom',
|
|
showLegend: true,
|
|
},
|
|
tooltip: {
|
|
maxHeight: 600,
|
|
mode: 'single',
|
|
sort: 'none',
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
const panel = new DashboardGridItem({
|
|
body: libVizPanel,
|
|
y: 0,
|
|
x: 0,
|
|
width: 12,
|
|
height: 8,
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.id).toBe(4);
|
|
expect(result.libraryPanel).toEqual({
|
|
name: 'Some lib panel panel',
|
|
uid: 'lib-panel-uid',
|
|
});
|
|
expect(result.gridPos).toEqual({
|
|
h: 8,
|
|
w: 12,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
expect(result.title).toBe('A panel');
|
|
expect(result.transformations).toBeUndefined();
|
|
expect(result.fieldConfig).toBeUndefined();
|
|
expect(result.options).toBeUndefined();
|
|
});
|
|
|
|
it('given a library panel widget', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
id: 4,
|
|
gridPos: {
|
|
h: 8,
|
|
w: 12,
|
|
x: 0,
|
|
y: 0,
|
|
},
|
|
type: 'add-library-panel',
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.id).toBe(4);
|
|
expect(result.gridPos).toEqual({
|
|
h: 8,
|
|
w: 12,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
expect(result.type).toBe('add-library-panel');
|
|
});
|
|
});
|
|
|
|
describe('Annotations', () => {
|
|
it('should transform annotations to save model', () => {
|
|
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
|
|
expect(saveModel.annotations?.list?.length).toBe(4);
|
|
expect(saveModel.annotations?.list).toMatchSnapshot();
|
|
});
|
|
|
|
it('should transform annotations to save model after state changes', () => {
|
|
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
|
|
|
const layers = (scene.state.$data as DashboardDataLayerSet)?.state.annotationLayers;
|
|
const enabledLayer = layers[1];
|
|
const hiddenLayer = layers[3];
|
|
|
|
enabledLayer.setState({
|
|
isEnabled: false,
|
|
});
|
|
hiddenLayer.setState({
|
|
isHidden: false,
|
|
});
|
|
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
|
|
expect(saveModel.annotations?.list?.length).toBe(4);
|
|
expect(saveModel.annotations?.list?.[1].enable).toEqual(false);
|
|
expect(saveModel.annotations?.list?.[3].hide).toEqual(false);
|
|
});
|
|
});
|
|
|
|
describe('Queries', () => {
|
|
it('Given panel with queries', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
maxDataPoints: 100,
|
|
targets: [
|
|
{
|
|
refId: 'A',
|
|
expr: 'A',
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
},
|
|
{
|
|
refId: 'B',
|
|
expr: 'B',
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.maxDataPoints).toBe(100);
|
|
expect(result.targets?.length).toBe(2);
|
|
expect(result.targets?.[0]).toEqual({
|
|
refId: 'A',
|
|
expr: 'A',
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
});
|
|
|
|
expect(result.datasource).toEqual({
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
});
|
|
});
|
|
|
|
it('Given panel with transformations', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
maxDataPoints: 100,
|
|
|
|
transformations: [
|
|
{
|
|
id: 'reduce',
|
|
options: {
|
|
reducers: ['max'],
|
|
mode: 'reduceFields',
|
|
includeTimeField: false,
|
|
},
|
|
},
|
|
],
|
|
|
|
targets: [
|
|
{
|
|
refId: 'A',
|
|
expr: 'A',
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
},
|
|
{
|
|
refId: 'B',
|
|
expr: 'B',
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.transformations?.length).toBe(1);
|
|
|
|
expect(result.maxDataPoints).toBe(100);
|
|
expect(result.targets?.length).toBe(2);
|
|
expect(result.targets?.[0]).toEqual({
|
|
refId: 'A',
|
|
expr: 'A',
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
});
|
|
|
|
expect(result.datasource).toEqual({
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
});
|
|
});
|
|
it('Given panel with shared query', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
targets: [
|
|
{
|
|
refId: 'A',
|
|
panelId: 1,
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.targets?.length).toBe(1);
|
|
expect(result.targets?.[0]).toEqual({
|
|
refId: 'A',
|
|
panelId: 1,
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
});
|
|
|
|
expect(result.datasource).toEqual({
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
});
|
|
});
|
|
|
|
it('Given panel with shared query and transformations', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
targets: [
|
|
{
|
|
refId: 'A',
|
|
panelId: 1,
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
},
|
|
],
|
|
transformations: [
|
|
{
|
|
id: 'reduce',
|
|
options: {
|
|
reducers: ['max'],
|
|
mode: 'reduceFields',
|
|
includeTimeField: false,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.transformations?.length).toBe(1);
|
|
|
|
expect(result.targets?.length).toBe(1);
|
|
expect(result.targets?.[0]).toEqual({
|
|
refId: 'A',
|
|
panelId: 1,
|
|
datasource: {
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
},
|
|
});
|
|
|
|
expect(result.datasource).toEqual({
|
|
type: 'datasource',
|
|
uid: SHARED_DASHBOARD_QUERY,
|
|
});
|
|
});
|
|
|
|
it('Given panel with query caching options', () => {
|
|
const panel = buildGridItemFromPanelSchema({
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
cacheTimeout: '10',
|
|
queryCachingTTL: 200000,
|
|
maxDataPoints: 100,
|
|
targets: [
|
|
{
|
|
refId: 'A',
|
|
expr: 'A',
|
|
datasource: {
|
|
type: 'grafana-testdata',
|
|
uid: 'abc',
|
|
},
|
|
},
|
|
{
|
|
refId: 'B',
|
|
expr: 'B',
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = gridItemToPanel(panel);
|
|
|
|
expect(result.cacheTimeout).toBe('10');
|
|
expect(result.queryCachingTTL).toBe(200000);
|
|
});
|
|
});
|
|
|
|
describe('Snapshots', () => {
|
|
const fakeCurrentDate = dateTime('2023-01-01T20:00:00.000Z').toDate();
|
|
|
|
beforeEach(() => {
|
|
advanceTo(fakeCurrentDate);
|
|
});
|
|
|
|
it('attaches snapshot data to panels using Grafana snapshot query', async () => {
|
|
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
await new Promise((r) => setTimeout(r, 1));
|
|
|
|
const snapshot = transformSceneToSaveModel(scene, true);
|
|
|
|
expect(snapshot.panels?.length).toBe(3);
|
|
|
|
// Regular panel with SceneQueryRunner
|
|
expect(snapshot.panels?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[0].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[0].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[100, 200, 300],
|
|
[1, 2, 3],
|
|
],
|
|
});
|
|
|
|
// Panel with transformations
|
|
expect(snapshot.panels?.[1].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[100, 200, 300],
|
|
[10, 20, 30],
|
|
],
|
|
});
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].transformations).toEqual([
|
|
{
|
|
id: 'reduce',
|
|
options: {},
|
|
},
|
|
]);
|
|
|
|
// Panel with a shared query (dahsboard query)
|
|
expect(snapshot.panels?.[2].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[2].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[2].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[100, 200, 300],
|
|
[1, 2, 3],
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles basic rows', async () => {
|
|
const scene = transformSaveModelToScene({ dashboard: snapshotableWithRowsDashboardJson as any, meta: {} });
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
await new Promise((r) => setTimeout(r, 1));
|
|
|
|
const snapshot = transformSceneToSaveModel(scene, true);
|
|
|
|
expect(snapshot.panels?.length).toBe(5);
|
|
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[0].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[0].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[100, 200, 300],
|
|
[1, 2, 3],
|
|
],
|
|
});
|
|
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].targets).toBeUndefined();
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].panels).toEqual([]);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[1].collapsed).toEqual(false);
|
|
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[2].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[2].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[100, 200, 300],
|
|
[10, 20, 30],
|
|
],
|
|
});
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[3].targets?.[0].datasource).toEqual(GRAFANA_DATASOURCE_REF);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[3].targets?.[0].snapshot[0].data).toEqual({
|
|
values: [
|
|
[1000, 2000, 3000],
|
|
[100, 200, 300],
|
|
],
|
|
});
|
|
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[4].targets).toBeUndefined();
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[4].panels).toHaveLength(1);
|
|
// @ts-expect-error
|
|
expect(snapshot.panels?.[4].collapsed).toEqual(true);
|
|
});
|
|
|
|
describe('repeats', () => {
|
|
it('handles repeated panels', async () => {
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, numberOfOptions: 2 });
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(2);
|
|
const result = panelRepeaterToPanels(repeater, undefined, true);
|
|
|
|
expect(result).toHaveLength(2);
|
|
|
|
// @ts-expect-error
|
|
expect(result[0].scopedVars).toEqual({
|
|
server: {
|
|
text: 'A',
|
|
value: '1',
|
|
},
|
|
});
|
|
// @ts-expect-error
|
|
expect(result[1].scopedVars).toEqual({
|
|
server: {
|
|
text: 'B',
|
|
value: '2',
|
|
},
|
|
});
|
|
|
|
expect(result[0].title).toEqual('Panel $server');
|
|
expect(result[1].title).toEqual('Panel $server');
|
|
});
|
|
|
|
it('handles repeated library panels', () => {
|
|
const { scene, repeater } = buildPanelRepeaterScene(
|
|
{ variableQueryTime: 0, numberOfOptions: 2 },
|
|
new LibraryVizPanel({
|
|
name: 'Some lib panel panel',
|
|
title: 'A panel',
|
|
uid: 'lib-panel-uid',
|
|
panelKey: 'lib-panel',
|
|
panel: new VizPanel({
|
|
key: 'panel-4',
|
|
title: 'Panel blahh blah',
|
|
fieldConfig: {
|
|
defaults: {},
|
|
overrides: [],
|
|
},
|
|
options: {
|
|
legend: {
|
|
calcs: [],
|
|
displayMode: 'list',
|
|
placement: 'bottom',
|
|
showLegend: true,
|
|
},
|
|
tooltip: {
|
|
maxHeight: 600,
|
|
mode: 'single',
|
|
sort: 'none',
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
);
|
|
|
|
activateFullSceneTree(scene);
|
|
const result = panelRepeaterToPanels(repeater, undefined, true);
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
expect(result[0]).toMatchObject({
|
|
id: 4,
|
|
title: 'A panel',
|
|
libraryPanel: {
|
|
name: 'Some lib panel panel',
|
|
uid: 'lib-panel-uid',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('handles row repeats ', () => {
|
|
const { scene, row } = buildPanelRepeaterScene({
|
|
variableQueryTime: 0,
|
|
numberOfOptions: 2,
|
|
useRowRepeater: true,
|
|
usePanelRepeater: false,
|
|
});
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
let panels: Panel[] = [];
|
|
gridRowToSaveModel(row, panels, undefined, true);
|
|
|
|
expect(panels).toHaveLength(2);
|
|
expect(panels[0].repeat).toBe('handler');
|
|
|
|
// @ts-expect-error
|
|
expect(panels[0].scopedVars).toEqual({
|
|
handler: {
|
|
text: 'AA',
|
|
value: '11',
|
|
},
|
|
});
|
|
|
|
expect(panels[1].title).toEqual('Panel $server');
|
|
expect(panels[1].gridPos).toEqual({ x: 0, y: 0, w: 10, h: 10 });
|
|
});
|
|
|
|
it('handles row repeats with panel repeater', () => {
|
|
const { scene, row } = buildPanelRepeaterScene({
|
|
variableQueryTime: 0,
|
|
numberOfOptions: 2,
|
|
useRowRepeater: true,
|
|
usePanelRepeater: true,
|
|
});
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
let panels: Panel[] = [];
|
|
gridRowToSaveModel(row, panels, undefined, true);
|
|
|
|
expect(panels[0].repeat).toBe('handler');
|
|
|
|
// @ts-expect-error
|
|
expect(panels[0].scopedVars).toEqual({
|
|
handler: {
|
|
text: 'AA',
|
|
value: '11',
|
|
},
|
|
});
|
|
|
|
// @ts-expect-error
|
|
expect(panels[1].scopedVars).toEqual({
|
|
server: {
|
|
text: 'A',
|
|
value: '1',
|
|
},
|
|
});
|
|
// @ts-expect-error
|
|
expect(panels[2].scopedVars).toEqual({
|
|
server: {
|
|
text: 'B',
|
|
value: '2',
|
|
},
|
|
});
|
|
|
|
expect(panels[1].title).toEqual('Panel $server');
|
|
expect(panels[2].title).toEqual('Panel $server');
|
|
});
|
|
});
|
|
|
|
describe('trimDashboardForSnapshot', () => {
|
|
let snapshot: Dashboard = {} as Dashboard;
|
|
|
|
beforeEach(() => {
|
|
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
|
activateFullSceneTree(scene);
|
|
snapshot = transformSceneToSaveModel(scene, true);
|
|
});
|
|
|
|
it('should not mutate provided dashboard', () => {
|
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
|
expect(result).not.toBe(snapshot);
|
|
});
|
|
|
|
it('should apply provided title and absolute time range', async () => {
|
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
|
|
|
expect(result.title).toBe('Snap title');
|
|
expect(result.time).toBeDefined();
|
|
expect(result.time!.from).toEqual('2023-01-01T14:00:00.000Z');
|
|
expect(result.time!.to).toEqual('2023-01-01T20:00:00.000Z');
|
|
});
|
|
|
|
it('should remove queries from annotations and attach empty snapshotData', () => {
|
|
expect(snapshot.annotations?.list?.[0].target).toBeDefined();
|
|
expect(snapshot.annotations?.list?.[1].target).toBeDefined();
|
|
|
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
|
|
|
expect(result.annotations?.list?.length).toBe(2);
|
|
expect(result.annotations?.list?.[0].target).toBeUndefined();
|
|
expect(result.annotations?.list?.[0].snapshotData).toEqual([]);
|
|
expect(result.annotations?.list?.[1].target).toBeUndefined();
|
|
expect(result.annotations?.list?.[1].snapshotData).toEqual([]);
|
|
});
|
|
it('should remove queries from variables', () => {
|
|
expect(snapshot.templating?.list?.length).toBe(1);
|
|
|
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
|
|
|
expect(result.templating?.list?.length).toBe(1);
|
|
expect(result.templating?.list?.[0].query).toBe('');
|
|
expect(result.templating?.list?.[0].refresh).toBe(VariableRefresh.never);
|
|
expect(result.templating?.list?.[0].options).toHaveLength(1);
|
|
expect(result.templating?.list?.[0].options?.[0]).toEqual({
|
|
text: 'annotations',
|
|
value: 'annotations',
|
|
});
|
|
});
|
|
|
|
it('should snapshot a single panel when provided', () => {
|
|
const vizPanel = new VizPanel({
|
|
key: getVizPanelKeyForPanelId(2),
|
|
});
|
|
|
|
const result = trimDashboardForSnapshot(
|
|
'Snap title',
|
|
getTimeRange({ from: 'now-6h', to: 'now' }),
|
|
snapshot,
|
|
vizPanel
|
|
);
|
|
|
|
expect(snapshot.panels?.length).toBe(3);
|
|
expect(result.panels?.length).toBe(1);
|
|
expect(result.panels?.[0].gridPos).toEqual({ w: 24, x: 0, y: 0, h: 20 });
|
|
});
|
|
|
|
it('should remove links', async () => {
|
|
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
|
activateFullSceneTree(scene);
|
|
const snapshot = transformSceneToSaveModel(scene, true);
|
|
expect(snapshot.links?.length).toBe(1);
|
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
|
expect(result.links?.length).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Given a scene with an open panel editor', () => {
|
|
it('should persist changes to panel model', async () => {
|
|
const panel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'text',
|
|
});
|
|
|
|
const gridItem = new DashboardGridItem({ body: panel });
|
|
|
|
const editScene = buildPanelEditScene(panel);
|
|
const scene = new DashboardScene({
|
|
editPanel: editScene,
|
|
isEditing: true,
|
|
body: new SceneGridLayout({
|
|
children: [gridItem],
|
|
}),
|
|
$timeRange: new SceneTimeRange({
|
|
from: 'now-6h',
|
|
to: 'now',
|
|
timeZone: '',
|
|
}),
|
|
});
|
|
|
|
editScene!.state.vizManager.state.panel.setState({
|
|
options: {
|
|
mode: 'markdown',
|
|
code: {
|
|
language: 'plaintext',
|
|
showLineNumbers: false,
|
|
showMiniMap: false,
|
|
},
|
|
content: 'new content',
|
|
},
|
|
});
|
|
activateFullSceneTree(scene);
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
expect((saveModel.panels![0] as any).options.content).toBe('new content');
|
|
});
|
|
|
|
it('should persist changes to panel model in row', async () => {
|
|
const panel = new VizPanel({
|
|
key: 'panel-1',
|
|
pluginId: 'text',
|
|
options: {
|
|
content: 'old content',
|
|
},
|
|
});
|
|
|
|
const gridItem = new DashboardGridItem({ body: panel });
|
|
|
|
const editScene = buildPanelEditScene(panel);
|
|
const scene = new DashboardScene({
|
|
editPanel: editScene,
|
|
isEditing: true,
|
|
body: new SceneGridLayout({
|
|
children: [
|
|
new SceneGridRow({
|
|
key: '23',
|
|
isCollapsed: false,
|
|
children: [gridItem],
|
|
}),
|
|
],
|
|
}),
|
|
$timeRange: new SceneTimeRange({
|
|
from: 'now-6h',
|
|
to: 'now',
|
|
timeZone: '',
|
|
}),
|
|
});
|
|
activateFullSceneTree(scene);
|
|
|
|
editScene!.state.vizManager.state.panel.setState({
|
|
options: {
|
|
mode: 'markdown',
|
|
code: {
|
|
language: 'plaintext',
|
|
showLineNumbers: false,
|
|
showMiniMap: false,
|
|
},
|
|
content: 'new content',
|
|
},
|
|
});
|
|
|
|
const saveModel = transformSceneToSaveModel(scene);
|
|
expect((saveModel.panels![1] as any).options.content).toBe('new content');
|
|
});
|
|
});
|
|
});
|
|
|
|
export function buildGridItemFromPanelSchema(panel: Partial<Panel>) {
|
|
if (panel.libraryPanel) {
|
|
return buildGridItemForLibPanel(new PanelModel(panel))!;
|
|
}
|
|
|
|
return buildGridItemForPanel(new PanelModel(panel));
|
|
}
|
|
|