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/serialization/transformSceneToSaveModel.t...

1089 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';
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes';
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
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/constants';
import { DashboardDataDTO } from 'app/types';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/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 { 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,
},
}),
// mock getInstanceSettings()
getInstanceSettings: jest.fn(),
}),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
},
config: {
...jest.requireActual('@grafana/runtime').config,
panels: {
text: { skipDataQuery: true },
},
featureToggles: {
dataTrails: false,
},
theme2: {
visualization: {
getColorByName: jest.fn().mockReturnValue('red'),
},
},
},
setPluginExtensionGetter: jest.fn(),
getPluginLinkExtensions: jest.fn(),
}));
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
setWeekStart: 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'],
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
};
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as DashboardDataDTO, 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 DashboardDataDTO, 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 DashboardDataDTO,
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 DashboardDataDTO,
meta: {},
});
const variable = scene.state.$variables?.state.variables[0] as MultiValueVariable;
variable.changeValueTo(['a', 'b', 'c']);
const layout = scene.state.body as DefaultGridLayoutManager;
const grid = layout.state.grid;
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', () => {
const libVizPanel = new VizPanel({
key: 'panel-4',
title: 'Panel blahh blah',
$behaviors: [
new LibraryPanelBehavior({
name: 'Some lib panel panel',
uid: 'lib-panel-uid',
}),
],
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('Panel blahh blah');
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 DashboardDataDTO, 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 DashboardDataDTO, 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 DashboardDataDTO, 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 DashboardDataDTO,
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, 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 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',
},
},
$behaviors: [
new LibraryPanelBehavior({
name: 'Some lib panel panel',
uid: 'lib-panel-uid',
}),
],
})
);
activateFullSceneTree(scene);
const result = panelRepeaterToPanels(repeater, true);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: 4,
title: 'Panel blahh blah',
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, 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, 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 DashboardDataDTO, 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 DashboardDataDTO, 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 repeated panels and non-repeated panels', () => {
it('should save repeated panels itemHeight as height', () => {
const scene = transformSaveModelToScene({
dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO,
meta: {},
});
const gridItem = sceneGraph.findByKey(scene, 'grid-item-2') as DashboardGridItem;
expect(gridItem).toBeInstanceOf(DashboardGridItem);
expect(gridItem.state.height).toBe(10);
expect(gridItem.state.itemHeight).toBe(10);
expect(gridItem.state.itemHeight).toBe(10);
expect(gridItem.state.variableName).toBe('pod');
gridItem.setState({ itemHeight: 24 });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel.panels?.[3].gridPos?.h).toBe(24);
});
it('should not save non-repeated panels itemHeight as height', () => {
const scene = transformSaveModelToScene({
dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO,
meta: {},
});
const gridItem = sceneGraph.findByKey(scene, 'grid-item-15') as DashboardGridItem;
expect(gridItem).toBeInstanceOf(DashboardGridItem);
expect(gridItem.state.height).toBe(2);
expect(gridItem.state.itemHeight).toBe(2);
expect(gridItem.state.variableName).toBeUndefined();
gridItem.setState({ itemHeight: 24 });
let saveModel = transformSceneToSaveModel(scene);
expect(saveModel.panels?.[1].gridPos?.h).toBe(2);
gridItem.setState({ height: 34 });
saveModel = transformSceneToSaveModel(scene);
expect(saveModel.panels?.[1].gridPos?.h).toBe(34);
});
});
});
describe('Given a scene with custom quick ranges', () => {
it('should save quick ranges to save model', () => {
const dashboardWithCustomSettings = {
...dashboard_to_load1,
timepicker: {
...dashboard_to_load1.timepicker,
quick_ranges: [
{
display: 'Last 6 hours',
from: 'now-6h',
to: 'now',
},
{
display: 'Last 3 days',
from: 'now-3d',
to: 'now',
},
],
},
};
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as DashboardDataDTO, meta: {} });
const saveModel = transformSceneToSaveModel(scene);
expect(saveModel).toMatchSnapshot();
});
});
export function buildGridItemFromPanelSchema(panel: Partial<Panel>) {
return buildGridItemForPanel(new PanelModel(panel));
}