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/transformSaveModelToScene.t...

1029 lines
36 KiB

import { LoadingState } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { config } from '@grafana/runtime';
import {
AdHocFiltersVariable,
behaviors,
ConstantVariable,
SceneDataTransformer,
SceneGridItem,
SceneGridLayout,
SceneGridRow,
SceneQueryRunner,
VizPanel,
} from '@grafana/scenes';
import {
DashboardCursorSync,
defaultDashboard,
defaultTimePickerConfig,
Panel,
RowPanel,
VariableType,
} from '@grafana/schema';
import { contextSrv } from 'app/core/core';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
import { DashboardDataDTO } from 'app/types/dashboard';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { NEW_LINK } from '../settings/links/utils';
import { getQueryRunnerFor } from '../utils/utils';
import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel';
import { GRAFANA_DATASOURCE_REF } from './const';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
import rowsAfterFreePanels from './testfiles/rows_after_free_panels.json';
import {
createDashboardSceneFromDashboardModel,
buildGridItemForPanel,
transformSaveModelToScene,
convertOldSnapshotToScenesSnapshot,
} from './transformSaveModelToScene';
describe('transformSaveModelToScene', () => {
describe('when creating dashboard scene', () => {
it('should initialize the DashboardScene with the model state', () => {
const dash = {
...defaultDashboard,
title: 'test',
uid: 'test-uid',
time: { from: 'now-10h', to: 'now' },
weekStart: 'saturday',
fiscalYearStartMonth: 2,
timezone: 'America/New_York',
timepicker: {
...defaultTimePickerConfig,
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
templating: {
list: [
{
hide: 2,
name: 'constant',
skipUrlSync: false,
type: 'constant' as VariableType,
query: 'test',
id: 'constant',
global: false,
index: 3,
state: LoadingState.Done,
error: null,
description: '',
datasource: null,
},
{
hide: 2,
name: 'CoolFilters',
type: 'adhoc' as VariableType,
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
id: 'adhoc',
global: false,
skipUrlSync: false,
index: 3,
state: LoadingState.Done,
error: null,
description: '',
},
],
},
};
const oldModel = new DashboardModel(dash);
const scene = createDashboardSceneFromDashboardModel(oldModel, dash);
const dashboardControls = scene.state.controls!;
expect(scene.state.title).toBe('test');
expect(scene.state.uid).toBe('test-uid');
expect(scene.state.links).toHaveLength(1);
expect(scene.state.links![0].title).toBe('Link 1');
expect(scene.state?.$timeRange?.state.value.raw).toEqual(dash.time);
expect(scene.state?.$timeRange?.state.fiscalYearStartMonth).toEqual(2);
expect(scene.state?.$timeRange?.state.timeZone).toEqual('America/New_York');
expect(scene.state?.$timeRange?.state.weekStart).toEqual('saturday');
expect(scene.state?.$variables?.state.variables).toHaveLength(2);
expect(scene.state?.$variables?.getByName('constant')).toBeInstanceOf(ConstantVariable);
expect(scene.state?.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable);
expect(
(scene.state?.$variables?.getByName('CoolFilters') as AdHocFiltersVariable).state.useQueriesAsFilterForOptions
).toBe(true);
expect(dashboardControls).toBeDefined();
expect(dashboardControls.state.refreshPicker.state.intervals).toEqual(defaultTimePickerConfig.refresh_intervals);
expect(dashboardControls.state.hideTimeControls).toBe(true);
});
it('should apply cursor sync behavior', () => {
const dash = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
graphTooltip: DashboardCursorSync.Crosshair,
};
const oldModel = new DashboardModel(dash);
const scene = createDashboardSceneFromDashboardModel(oldModel, dash);
const cursorSync = scene.state.$behaviors?.find((b) => b instanceof behaviors.CursorSync);
expect(cursorSync).toBeInstanceOf(behaviors.CursorSync);
expect((cursorSync as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair);
});
it('should apply live now timer behavior', () => {
const dash = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
};
const oldModel = new DashboardModel(dash);
const scene = createDashboardSceneFromDashboardModel(oldModel, dash);
const liveNowTimer = scene.state.$behaviors?.find((b) => b instanceof behaviors.LiveNowTimer);
expect(liveNowTimer).toBeInstanceOf(behaviors.LiveNowTimer);
});
it('should initialize the Dashboard Scene with empty template variables', () => {
const dash = {
...defaultDashboard,
title: 'test empty dashboard with no variables',
uid: 'test-uid',
time: { from: 'now-10h', to: 'now' },
weekStart: 'saturday',
fiscalYearStartMonth: 2,
timezone: 'America/New_York',
templating: {
list: [],
},
};
const oldModel = new DashboardModel(dash);
const scene = createDashboardSceneFromDashboardModel(oldModel, dash);
expect(scene.state.$variables?.state.variables).toBeDefined();
});
it('should not return lazy loaded panels when user is image renderer', () => {
contextSrv.user.authenticatedBy = 'render';
const panel1 = createPanelSaveModel({
title: 'test1',
gridPos: { x: 0, y: 1, w: 12, h: 8 },
}) as Panel;
const panel2 = createPanelSaveModel({
title: 'test2',
gridPos: { x: 0, y: 10, w: 12, h: 8 },
}) as Panel;
const dashboard = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
panels: [panel1, panel2],
};
const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard);
const layout = scene.state.body as DefaultGridLayoutManager;
const body = layout.state.grid;
expect(body.state.isLazy).toBeFalsy();
});
});
describe('When creating a new dashboard', () => {
it('should initialize the DashboardScene in edit mode and dirty', async () => {
const rsp = await buildNewDashboardSaveModel();
const scene = transformSaveModelToScene(rsp);
expect(scene.state.isEditing).toBe(undefined);
expect(scene.state.isDirty).toBe(false);
});
});
describe('When creating a snapshot dashboard scene', () => {
it('should initialize a dashboard scene with SnapshotVariables', () => {
const customVariable = {
current: {
selected: false,
text: 'a',
value: 'a',
},
hide: 0,
includeAll: false,
multi: false,
name: 'custom0',
options: [],
query: 'a,b,c,d',
skipUrlSync: false,
type: 'custom' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const intervalVariable = {
current: {
selected: false,
text: '10s',
value: '10s',
},
hide: 0,
includeAll: false,
multi: false,
name: 'interval0',
options: [],
query: '10s,20s,30s',
skipUrlSync: false,
type: 'interval' as VariableType,
rootStateKey: 'N4XLmH5Vz',
};
const adHocVariable = {
global: false,
name: 'CoolFilters',
label: 'CoolFilters Label',
type: 'adhoc' as VariableType,
datasource: {
uid: 'gdev-prometheus',
type: 'prometheus',
},
filters: [
{
key: 'filterTest',
operator: '=',
value: 'test',
},
],
baseFilters: [
{
key: 'baseFilterTest',
operator: '=',
value: 'test',
},
],
hide: 0,
index: 0,
};
const snapshot = {
...defaultDashboard,
title: 'snapshot dash',
uid: 'test-uid',
time: { from: 'now-10h', to: 'now' },
weekStart: 'saturday',
fiscalYearStartMonth: 2,
timezone: 'America/New_York',
timepicker: {
...defaultTimePickerConfig,
hidden: true,
},
links: [{ ...NEW_LINK, title: 'Link 1' }],
templating: {
list: [customVariable, adHocVariable, intervalVariable],
},
};
const oldModel = new DashboardModel(snapshot, { isSnapshot: true });
const scene = createDashboardSceneFromDashboardModel(oldModel, snapshot);
// check variables were converted to snapshot variables
expect(scene.state.$variables?.state.variables).toHaveLength(3);
expect(scene.state.$variables?.getByName('custom0')).toBeInstanceOf(SnapshotVariable);
expect(scene.state.$variables?.getByName('CoolFilters')).toBeInstanceOf(AdHocFiltersVariable);
expect(scene.state.$variables?.getByName('interval0')).toBeInstanceOf(SnapshotVariable);
// custom snapshot
const customSnapshot = scene.state.$variables?.getByName('custom0') as SnapshotVariable;
expect(customSnapshot.state.value).toBe('a');
expect(customSnapshot.state.text).toBe('a');
expect(customSnapshot.state.isReadOnly).toBe(true);
// adhoc snapshot
const adhocSnapshot = scene.state.$variables?.getByName('CoolFilters') as AdHocFiltersVariable;
expect(adhocSnapshot.state.filters).toEqual(adHocVariable.filters);
expect(adhocSnapshot.state.readOnly).toBe(true);
// interval snapshot
const intervalSnapshot = scene.state.$variables?.getByName('interval0') as SnapshotVariable;
expect(intervalSnapshot.state.value).toBe('10s');
expect(intervalSnapshot.state.text).toBe('10s');
expect(intervalSnapshot.state.isReadOnly).toBe(true);
});
});
describe('when organizing panels as scene children', () => {
it('should leave panels outside second row if it is collapsed', () => {
const panel1 = createPanelSaveModel({
title: 'test1',
gridPos: { x: 0, y: 1, w: 12, h: 8 },
}) as Panel;
const panel2 = createPanelSaveModel({
title: 'test2',
gridPos: { x: 0, y: 10, w: 12, h: 8 },
}) as Panel;
const row1 = createPanelSaveModel({
title: 'test row 1',
type: 'row',
gridPos: { x: 0, y: 0, w: 12, h: 1 },
collapsed: false,
panels: [],
}) as unknown as RowPanel;
const row2 = createPanelSaveModel({
title: 'test row 2',
type: 'row',
gridPos: { x: 0, y: 9, w: 12, h: 1 },
collapsed: true,
panels: [],
}) as unknown as RowPanel;
const dashboard = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
panels: [row1, panel1, row2, panel2],
};
const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard);
const layout = scene.state.body as DefaultGridLayoutManager;
const body = layout.state.grid;
expect(body.state.children).toHaveLength(3);
const rowScene1 = body.state.children[0] as SceneGridRow;
expect(rowScene1).toBeInstanceOf(SceneGridRow);
expect(rowScene1.state.title).toEqual(row1.title);
expect(rowScene1.state.isCollapsed).toEqual(row1.collapsed);
expect(rowScene1.state.children).toHaveLength(1);
expect(rowScene1.state.children[0]).toBeInstanceOf(DashboardGridItem);
const rowScene2 = body.state.children[1] as SceneGridRow;
expect(rowScene2).toBeInstanceOf(SceneGridRow);
expect(rowScene2.state.title).toEqual(row2.title);
expect(rowScene2.state.isCollapsed).toEqual(row2.collapsed);
expect(rowScene2.state.children).toHaveLength(0);
expect(body.state.children[2]).toBeInstanceOf(DashboardGridItem);
});
it('should create panels within collapsed rows', () => {
const panel = createPanelSaveModel({
title: 'test',
gridPos: { x: 1, y: 0, w: 12, h: 8 },
}) as Panel;
const libPanel = createPanelSaveModel({
title: 'Library Panel',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
libraryPanel: {
uid: '123',
name: 'My Panel',
},
});
const row = createPanelSaveModel({
title: 'test',
type: 'row',
gridPos: { x: 0, y: 0, w: 12, h: 1 },
collapsed: true,
panels: [panel, libPanel],
}) as unknown as RowPanel;
const dashboard = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
panels: [row],
};
const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard);
const layout = scene.state.body as DefaultGridLayoutManager;
const body = layout.state.grid;
expect(body.state.children).toHaveLength(1);
const rowScene = body.state.children[0] as SceneGridRow;
expect(rowScene).toBeInstanceOf(SceneGridRow);
expect(rowScene.state.title).toEqual(row.title);
expect(rowScene.state.y).toEqual(row.gridPos!.y);
expect(rowScene.state.isCollapsed).toEqual(row.collapsed);
expect(rowScene.state.children).toHaveLength(2);
expect(rowScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
expect(rowScene.state.children[1]).toBeInstanceOf(DashboardGridItem);
// Panels are sorted by position in the row
expect((rowScene.state.children[0] as DashboardGridItem).state.body.state.$behaviors![0]).toBeInstanceOf(
LibraryPanelBehavior
);
expect((rowScene.state.children[1] as DashboardGridItem).state.body!).toBeInstanceOf(VizPanel);
});
it('should create panels within expanded row', () => {
const panelOutOfRow = createPanelSaveModel({
title: 'Out of a row',
gridPos: {
h: 8,
w: 12,
x: 0,
y: 0,
},
});
const libPanelOutOfRow = createPanelSaveModel({
title: 'Library Panel',
gridPos: { x: 0, y: 8, w: 12, h: 8 },
libraryPanel: {
uid: '123',
name: 'My Panel',
},
});
const rowWithPanel = createPanelSaveModel({
title: 'Row with panel',
type: 'row',
id: 10,
collapsed: false,
gridPos: {
h: 1,
w: 24,
x: 0,
y: 16,
},
// This panels array is not used if the row is not collapsed
panels: [],
});
const panelInRow = createPanelSaveModel({
gridPos: {
h: 8,
w: 12,
x: 0,
y: 17,
},
title: 'In row 1',
});
const libPanelInRow = createPanelSaveModel({
title: 'Library Panel',
gridPos: { x: 0, y: 25, w: 12, h: 8 },
libraryPanel: {
uid: '123',
name: 'My Panel',
},
});
const emptyRow = createPanelSaveModel({
collapsed: false,
gridPos: {
h: 1,
w: 24,
x: 0,
y: 26,
},
// This panels array is not used if the row is not collapsed
panels: [],
title: 'Empty row',
type: 'row',
});
const dashboard = {
...defaultDashboard,
title: 'Test dashboard',
uid: 'test-uid',
panels: [panelOutOfRow, libPanelOutOfRow, rowWithPanel, panelInRow, libPanelInRow, emptyRow],
};
const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard);
const layout = scene.state.body as DefaultGridLayoutManager;
const body = layout.state.grid;
expect(body.state.children).toHaveLength(4);
expect(body).toBeInstanceOf(SceneGridLayout);
// Panel out of row
expect(body.state.children[0]).toBeInstanceOf(DashboardGridItem);
const panelOutOfRowVizPanel = body.state.children[0] as DashboardGridItem;
expect((panelOutOfRowVizPanel.state.body as VizPanel)?.state.title).toBe(panelOutOfRow.title);
// lib panel out of row
expect(body.state.children[1]).toBeInstanceOf(DashboardGridItem);
const panelOutOfRowLibVizPanel = body.state.children[1] as DashboardGridItem;
expect(panelOutOfRowLibVizPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
// Row with panels
expect(body.state.children[2]).toBeInstanceOf(SceneGridRow);
const rowWithPanelsScene = body.state.children[2] as SceneGridRow;
expect(rowWithPanelsScene.state.title).toBe(rowWithPanel.title);
expect(rowWithPanelsScene.state.key).toBe('panel-10');
expect(rowWithPanelsScene.state.children).toHaveLength(2);
const libPanel = rowWithPanelsScene.state.children[1] as DashboardGridItem;
expect(libPanel.state.body.state.$behaviors![0]).toBeInstanceOf(LibraryPanelBehavior);
// Panel within row
expect(rowWithPanelsScene.state.children[0]).toBeInstanceOf(DashboardGridItem);
const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as DashboardGridItem;
expect((panelInRowVizPanel.state.body as VizPanel).state.title).toBe(panelInRow.title);
// Empty row
expect(body.state.children[3]).toBeInstanceOf(SceneGridRow);
const emptyRowScene = body.state.children[3] as SceneGridRow;
expect(emptyRowScene.state.title).toBe(emptyRow.title);
expect(emptyRowScene.state.children).toHaveLength(0);
});
});
describe('when creating viz panel objects', () => {
it('should initalize the VizPanel scene object state', () => {
const panel = {
title: 'test',
type: 'test-plugin',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
maxDataPoints: 100,
options: {
fieldOptions: {
defaults: {
unit: 'none',
decimals: 2,
},
overrides: [],
},
},
fieldConfig: {
defaults: {
unit: 'none',
},
overrides: [],
},
pluginVersion: '1.0.0',
transformations: [
{
id: 'reduce',
options: {
reducers: [
{
id: 'mean',
},
],
},
},
],
targets: [
{
refId: 'A',
queryType: 'randomWalk',
},
],
};
const { gridItem, vizPanel } = buildGridItemForTest(panel);
expect(gridItem.state.x).toEqual(0);
expect(gridItem.state.y).toEqual(0);
expect(gridItem.state.width).toEqual(12);
expect(gridItem.state.height).toEqual(8);
expect(vizPanel.state.title).toBe('test');
expect(vizPanel.state.pluginId).toBe('test-plugin');
expect(vizPanel.state.options).toEqual(panel.options);
expect(vizPanel.state.fieldConfig).toEqual(panel.fieldConfig);
expect(vizPanel.state.pluginVersion).toBe('1.0.0');
const queryRunner = getQueryRunnerFor(vizPanel)!;
expect(queryRunner.state.queries).toEqual(panel.targets);
expect(queryRunner.state.maxDataPoints).toEqual(100);
expect(queryRunner.state.maxDataPointsFromWidth).toEqual(true);
expect((vizPanel.state.$data as SceneDataTransformer)?.state.transformations).toEqual(panel.transformations);
});
it('should initalize the VizPanel without title and transparent true', () => {
const panel = {
title: '',
type: 'test-plugin',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
};
const { vizPanel } = buildGridItemForTest(panel);
expect(vizPanel.state.displayMode).toEqual('transparent');
expect(vizPanel.state.hoverHeader).toEqual(true);
});
it('should set hoverHeader to true if timeFrom and hideTimeOverride is true', () => {
const panel = {
type: 'test-plugin',
timeFrom: '2h',
hideTimeOverride: true,
};
const { vizPanel } = buildGridItemForTest(panel);
expect(vizPanel.state.hoverHeader).toBe(true);
});
it('should initalize the VizPanel with min interval set', () => {
const panel = {
title: '',
type: 'test-plugin',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
interval: '20m',
};
const { vizPanel } = buildGridItemForTest(panel);
const queryRunner = getQueryRunnerFor(vizPanel);
expect(queryRunner?.state.minInterval).toBe('20m');
});
it('should set PanelTimeRange when timeFrom or timeShift is present', () => {
const panel = {
type: 'test-plugin',
timeFrom: '2h',
timeShift: '1d',
};
const { vizPanel } = buildGridItemForTest(panel);
const timeRange = vizPanel.state.$timeRange as PanelTimeRange;
expect(timeRange).toBeInstanceOf(PanelTimeRange);
expect(timeRange.state.timeFrom).toBe('2h');
expect(timeRange.state.timeShift).toBe('1d');
});
it('should handle a dashboard query data source', () => {
const panel = {
title: '',
type: 'test-plugin',
datasource: { uid: SHARED_DASHBOARD_QUERY, type: DASHBOARD_DATASOURCE_PLUGIN_ID },
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
targets: [{ refId: 'A', panelId: 10 }],
};
const { vizPanel } = buildGridItemForTest(panel);
expect(vizPanel.state.$data).toBeInstanceOf(SceneDataTransformer);
expect(vizPanel.state.$data?.state.$data).toBeInstanceOf(SceneQueryRunner);
expect((vizPanel.state.$data?.state.$data as SceneQueryRunner).state.queries).toEqual(panel.targets);
});
it('should not set SceneQueryRunner for plugins with skipDataQuery', () => {
const panel = {
title: '',
type: 'text-plugin-34',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
targets: [{ refId: 'A' }],
};
config.panels['text-plugin-34'] = getPanelPlugin({
skipDataQuery: true,
}).meta;
const { vizPanel } = buildGridItemForTest(panel);
expect(vizPanel.state.$data).toBeUndefined();
});
it('When repeat is set but repeatDirection is not it should default to horizontal repeat', () => {
const panel = {
title: '',
type: 'text-plugin-34',
gridPos: { x: 0, y: 0, w: 8, h: 8 },
repeat: 'server',
maxPerRow: 8,
};
const gridItem = buildGridItemForPanel(new PanelModel(panel));
const repeater = gridItem as DashboardGridItem;
expect(repeater.state.maxPerRow).toBe(8);
expect(repeater.state.variableName).toBe('server');
expect(repeater.state.width).toBe(24);
expect(repeater.state.height).toBe(8);
expect(repeater.state.repeatDirection).toBe('h');
expect(repeater.state.maxPerRow).toBe(8);
});
it('When repeat is set should build PanelRepeaterGridItem', () => {
const panel = {
title: '',
type: 'text-plugin-34',
gridPos: { x: 0, y: 0, w: 8, h: 8 },
repeat: 'server',
repeatDirection: 'v',
maxPerRow: 8,
};
const gridItem = buildGridItemForPanel(new PanelModel(panel));
const repeater = gridItem as DashboardGridItem;
expect(repeater.state.maxPerRow).toBe(8);
expect(repeater.state.variableName).toBe('server');
expect(repeater.state.width).toBe(8);
expect(repeater.state.height).toBe(8);
expect(repeater.state.repeatDirection).toBe('v');
expect(repeater.state.maxPerRow).toBe(8);
});
it('When horizontal repeat is set should modify the width to 24', () => {
const panel = {
title: '',
type: 'text-plugin-34',
gridPos: { x: 0, y: 0, w: 8, h: 8 },
repeat: 'server',
repeatDirection: 'h',
maxPerRow: 8,
};
const gridItem = buildGridItemForPanel(new PanelModel(panel));
const repeater = gridItem as DashboardGridItem;
expect(repeater.state.maxPerRow).toBe(8);
expect(repeater.state.variableName).toBe('server');
expect(repeater.state.width).toBe(24);
expect(repeater.state.height).toBe(8);
expect(repeater.state.repeatDirection).toBe('h');
expect(repeater.state.maxPerRow).toBe(8);
});
it('When horizontal repeat is NOT fully configured should not modify the width', () => {
const panel = {
title: '',
type: 'text-plugin-34',
gridPos: { x: 0, y: 0, w: 8, h: 8 },
repeatDirection: 'h',
maxPerRow: 8,
};
const gridItem = buildGridItemForPanel(new PanelModel(panel));
const repeater = gridItem as DashboardGridItem;
expect(repeater.state.maxPerRow).toBe(8);
expect(repeater.state.variableName).toBe(undefined);
expect(repeater.state.width).toBe(8);
expect(repeater.state.height).toBe(8);
expect(repeater.state.repeatDirection).toBe(undefined);
expect(repeater.state.maxPerRow).toBe(8);
});
it('should apply query caching options to SceneQueryRunner', () => {
const panel = {
title: '',
type: 'test-plugin',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
cacheTimeout: '10',
queryCachingTTL: 200000,
};
const { vizPanel } = buildGridItemForTest(panel);
const runner = getQueryRunnerFor(vizPanel)!;
expect(runner.state.cacheTimeout).toBe('10');
expect(runner.state.queryCachingTTL).toBe(200000);
});
it('should convert saved lib panel to a viz panel with LibraryPanelBehavior', () => {
const panel = {
title: 'Panel',
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
libraryPanel: {
uid: '123',
name: 'My Panel',
folderUid: '456',
},
};
const gridItem = buildGridItemForPanel(new PanelModel(panel))!;
const libPanelBehavior = gridItem.state.body.state.$behaviors![0];
expect(libPanelBehavior).toBeInstanceOf(LibraryPanelBehavior);
expect((libPanelBehavior as LibraryPanelBehavior).state.uid).toEqual(panel.libraryPanel.uid);
expect((libPanelBehavior as LibraryPanelBehavior).state.name).toEqual(panel.libraryPanel.name);
expect(gridItem.state.body.state.title).toEqual(panel.title);
});
});
describe('Convert to new rows', () => {
beforeEach(() => {
// set feature flag to true
config.featureToggles.dashboardNewLayouts = true;
});
afterEach(() => {
config.featureToggles.dashboardNewLayouts = false;
});
it('Should convert legacy rows to new rows', () => {
const scene = transformSaveModelToScene({
dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO,
meta: {},
});
const layout = scene.state.body as RowsLayoutManager;
const row1 = layout.state.rows[0];
expect(row1.state.title).toBe('Row at the top - not repeated - saved expanded');
const row1Layout = row1.state.layout as DefaultGridLayoutManager;
expect(row1Layout.state.grid.state.children).toHaveLength(1);
const row1gridItem = row1Layout.state.grid.state.children[0] as SceneGridItem;
expect(row1gridItem.state.body).toBeInstanceOf(VizPanel);
const row1Panel = row1gridItem.state.body as VizPanel;
expect(row1Panel.state.pluginId).toBe('text');
const row1PanelOptions = row1Panel.state.options as { content: string };
expect(row1PanelOptions.content).toBe(
'<div class=\"center-vh\">\n Repeated row below. The row has \n a panel that is also repeated horizontally based\n on values in the $pod variable. \n</div>'
);
const row2 = layout.state.rows[1];
expect(row2.state.repeatByVariable).toBe('server');
const lastRow = layout.state.rows[layout.state.rows.length - 1];
expect(lastRow.state.title).toBe('Row at the bottom - not repeated - saved collapsed ');
const lastRowLayout = lastRow.state.layout as DefaultGridLayoutManager;
expect(lastRowLayout.state.grid.state.children).toHaveLength(1);
const lastRowgridItem = lastRowLayout.state.grid.state.children[0] as SceneGridItem;
expect(lastRowgridItem.state.body).toBeInstanceOf(VizPanel);
const lastRowPanel = lastRowgridItem.state.body as VizPanel;
expect(lastRowPanel.state.pluginId).toBe('text');
});
it('Should convert legacy rows to new rows with free panels before first row', () => {
const scene = transformSaveModelToScene({
dashboard: rowsAfterFreePanels as DashboardDataDTO,
meta: {},
});
const layout = scene.state.body as RowsLayoutManager;
const row1 = layout.state.rows[0];
expect(row1.state.title).toBe('');
expect(row1.state.hideHeader).toBe(true);
const row1Layout = row1.state.layout as DefaultGridLayoutManager;
expect(row1Layout.state.grid.state.children).toHaveLength(1);
});
});
describe('Repeating rows', () => {
it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({
dashboard: repeatingRowsAndPanelsDashboardJson as DashboardDataDTO,
meta: {},
});
const layout = scene.state.body as DefaultGridLayoutManager;
const body = layout.state.grid;
const row2 = body.state.children[1] as SceneGridRow;
expect(row2.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior);
const repeatBehavior = row2.state.$behaviors?.[0] as RowRepeaterBehavior;
expect(repeatBehavior.state.variableName).toBe('server');
const lastRow = body.state.children[body.state.children.length - 1] as SceneGridRow;
expect(lastRow.state.isCollapsed).toBe(true);
});
});
describe('Annotation queries', () => {
it('Should build correct scene model', () => {
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} });
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.annotationLayers).toHaveLength(4);
expect(dataLayers.state.annotationLayers[0].state.name).toBe('Annotations & Alerts');
expect(dataLayers.state.annotationLayers[0].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[0].state.isHidden).toBe(false);
expect(dataLayers.state.annotationLayers[1].state.name).toBe('Enabled');
expect(dataLayers.state.annotationLayers[1].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[1].state.isHidden).toBe(false);
expect(dataLayers.state.annotationLayers[2].state.name).toBe('Disabled');
expect(dataLayers.state.annotationLayers[2].state.isEnabled).toBe(false);
expect(dataLayers.state.annotationLayers[2].state.isHidden).toBe(false);
expect(dataLayers.state.annotationLayers[3].state.name).toBe('Hidden');
expect(dataLayers.state.annotationLayers[3].state.isEnabled).toBe(true);
expect(dataLayers.state.annotationLayers[3].state.isHidden).toBe(true);
});
});
describe('Alerting data layer', () => {
it('Should add alert states data layer if unified alerting enabled', () => {
config.unifiedAlertingEnabled = true;
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} });
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.alertStatesLayer).toBeDefined();
});
it('Should add alert states data layer if any panel has a legacy alert defined', () => {
config.unifiedAlertingEnabled = false;
const dashboard = { ...dashboard_to_load1 } as unknown as DashboardDataDTO;
dashboard.panels![0].alert = {};
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} });
expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet);
const dataLayers = scene.state.$data as DashboardDataLayerSet;
expect(dataLayers.state.alertStatesLayer).toBeDefined();
});
});
describe('when rendering a legacy snapshot as scene', () => {
it('should convert snapshotData to snapshot inside targets', () => {
const panel = createPanelSaveModel({
title: 'test',
gridPos: { x: 1, y: 0, w: 12, h: 8 },
// @ts-ignore
snapshotData: [
{
fields: [
{
name: 'Field 1',
type: 'time',
values: ['value1', 'value2'],
config: {},
},
{
name: 'Field 2',
type: 'number',
values: [1],
config: {},
},
],
},
],
}) as Panel;
const oldPanelModel = new PanelModel(panel);
convertOldSnapshotToScenesSnapshot(oldPanelModel);
expect(oldPanelModel.snapshotData?.length).toStrictEqual(0);
expect(oldPanelModel.targets.length).toStrictEqual(1);
expect(oldPanelModel.datasource).toStrictEqual(GRAFANA_DATASOURCE_REF);
expect(oldPanelModel.targets[0].datasource).toStrictEqual(GRAFANA_DATASOURCE_REF);
expect(oldPanelModel.targets[0].queryType).toStrictEqual('snapshot');
// @ts-ignore
expect(oldPanelModel.targets[0].snapshot.length).toBe(1);
// @ts-ignore
expect(oldPanelModel.targets[0].snapshot[0].data.values).toStrictEqual([['value1', 'value2'], [1]]);
// @ts-ignore
expect(oldPanelModel.targets[0].snapshot[0].schema.fields).toStrictEqual([
{ config: {}, name: 'Field 1', type: 'time' },
{ config: {}, name: 'Field 2', type: 'number' },
]);
});
});
});
describe('When creating a snapshot dashboard scene', () => {
it('should initialize a dashboard scene with SnapshotVariables', () => {
const dashboard = {
...defaultDashboard,
title: 'With custom quick ranges',
uid: 'test-uid',
timepicker: {
...defaultTimePickerConfig,
quick_ranges: [
{
display: 'Last 6 hours',
from: 'now-6h',
to: 'now',
},
{
display: 'Last 3 days',
from: 'now-3d',
to: 'now',
},
],
},
};
const oldModel = new DashboardModel(dashboard);
const scene = createDashboardSceneFromDashboardModel(oldModel, dashboard);
expect(scene.state.controls?.state.timePicker.state.quickRanges).toBe(dashboard.timepicker.quick_ranges);
});
});
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: DashboardGridItem; vizPanel: VizPanel } {
const gridItem = buildGridItemForPanel(new PanelModel(saveModel));
if (gridItem instanceof DashboardGridItem) {
return { gridItem, vizPanel: gridItem.state.body as VizPanel };
}
throw new Error('buildGridItemForPanel to return DashboardGridItem');
}