import { VariableRefresh } from '@grafana/data'; import { FetchError } from '@grafana/runtime'; import { DeepPartial, EmbeddedScene, SceneDeactivationHandler, SceneGridLayout, SceneGridRow, SceneObject, SceneTimeRange, SceneVariableSet, TestVariable, VizPanel, } from '@grafana/scenes'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; import { DashboardDTO } from 'app/types/dashboard'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; export function setupLoadDashboardMock(rsp: DeepPartial, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp); const loadSnapshotMock = (spy || jest.fn()).mockResolvedValue(rsp); // disabling type checks since this is a test util // eslint-disable-next-line @typescript-eslint/consistent-type-assertions setDashboardLoaderSrv({ loadDashboard: loadDashboardMock, loadSnapshot: loadSnapshotMock, } as unknown as DashboardLoaderSrv); return loadDashboardMock; } export function setupLoadDashboardMockReject(rsp: DeepPartial, spy?: jest.Mock) { const loadDashboardMock = (spy || jest.fn()).mockRejectedValue(rsp); // disabling type checks since this is a test util // eslint-disable-next-line @typescript-eslint/consistent-type-assertions setDashboardLoaderSrv({ loadDashboard: loadDashboardMock, } as unknown as DashboardLoaderSrv); return loadDashboardMock; } export function setupLoadDashboardRuntimeErrorMock() { // disabling type checks since this is a test util // eslint-disable-next-line @typescript-eslint/consistent-type-assertions setDashboardLoaderSrv({ loadDashboard: () => { throw new Error('Runtime error'); }, } as unknown as DashboardLoaderSrv); } export function mockResizeObserver() { window.ResizeObserver = class ResizeObserver { constructor(callback: ResizeObserverCallback) { setTimeout(() => { callback( [ // disabling type checks since this is a test util // eslint-disable-next-line @typescript-eslint/consistent-type-assertions { contentRect: { x: 1, y: 2, width: 500, height: 500, top: 100, bottom: 0, left: 100, right: 0, }, } as ResizeObserverEntry, ], this ); }); } observe() {} disconnect() {} unobserve() {} }; } /** * Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order * of React mount order and useEffect ordering. * */ export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler { const deactivationHandlers: SceneDeactivationHandler[] = []; // Important that variables are activated before other children if (scene.state.$variables) { deactivationHandlers.push(activateFullSceneTree(scene.state.$variables)); } scene.forEachChild((child) => { // For query runners which by default use the container width for maxDataPoints calculation we are setting a width. // In real life this is done by the React component when VizPanel is rendered. if ('setContainerWidth' in child) { // @ts-expect-error child.setContainerWidth(500); } deactivationHandlers.push(activateFullSceneTree(child)); }); deactivationHandlers.push(scene.activate()); return () => { for (const handler of deactivationHandlers) { handler(); } }; } interface SceneOptions { variableQueryTime: number; maxPerRow?: number; itemHeight?: number; repeatDirection?: RepeatDirection; x?: number; y?: number; numberOfOptions?: number; usePanelRepeater?: boolean; useRowRepeater?: boolean; throwError?: string; variableRefresh?: VariableRefresh; } export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel) { const defaults = { usePanelRepeater: true, ...options }; const withRepeat = new DashboardGridItem({ variableName: 'server', repeatedPanels: [], repeatDirection: options.repeatDirection, maxPerRow: options.maxPerRow, itemHeight: options.itemHeight, body: source ?? new VizPanel({ title: 'Panel $server', pluginId: 'timeseries', key: 'panel-1', }), x: options.x || 0, y: options.y || 0, }); const withoutRepeat = new DashboardGridItem({ x: 0, y: 0, width: 10, height: 10, body: new VizPanel({ title: 'Panel $server', pluginId: 'timeseries', titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], }), }); const row = new SceneGridRow({ $behaviors: defaults.useRowRepeater ? [new RowRepeaterBehavior({ variableName: 'handler' })] : [], children: [defaults.usePanelRepeater ? withRepeat : withoutRepeat], }); const panelRepeatVariable = new TestVariable({ name: 'server', query: 'A.*', value: ALL_VARIABLE_VALUE, text: ALL_VARIABLE_TEXT, isMulti: true, includeAll: true, delayMs: options.variableQueryTime, optionsToReturn: [ { label: 'A', value: '1' }, { label: 'B', value: '2' }, { label: 'C', value: '3' }, { label: 'D', value: '4' }, { label: 'E', value: '5' }, ].slice(0, options.numberOfOptions), throwError: defaults.throwError, refresh: options.variableRefresh, }); const rowRepeatVariable = new TestVariable({ name: 'handler', query: 'A.*', value: ALL_VARIABLE_VALUE, text: ALL_VARIABLE_TEXT, isMulti: true, includeAll: true, delayMs: options.variableQueryTime, optionsToReturn: [ { label: 'AA', value: '11' }, { label: 'BB', value: '22' }, { label: 'CC', value: '33' }, { label: 'DD', value: '44' }, { label: 'EE', value: '55' }, ].slice(0, options.numberOfOptions), throwError: defaults.throwError, }); const scene = new EmbeddedScene({ $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), $variables: new SceneVariableSet({ variables: [panelRepeatVariable, rowRepeatVariable], }), body: new DefaultGridLayoutManager({ grid: new SceneGridLayout({ children: [row], }), }), }); return { scene, repeater: withRepeat, row, variable: panelRepeatVariable }; }