Files
Torkel Ödegaard 3dccd29f89 Repeating: Minor refactoring and unification for DashboardGridItem and AutoGridItem (#109723)
* Repeating: Refactoring and unification

* Update

* update

* fix e2e

* fixes

* Update

* adjust e2e test

---------

Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
2025-08-18 12:02:45 +02:00

220 lines
6.7 KiB
TypeScript

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<DashboardDTO>, 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<FetchError>, 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 };
}