import { LoadingState } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; import { config } from '@grafana/runtime'; import { AdHocFiltersVariable, behaviors, ConstantVariable, SceneDataLayerControls, SceneDataTransformer, 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'; 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 { 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 { 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('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); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); 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); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); 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); expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); 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): { 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'); }