DashboardScene: Transform scene repeats to snapshot (#76428)

* WIP : Transform scene panel repeats to snapshot

* Set scoped vars correctly on repeated panel snapshot

* Row repeats snapshots

* Fix scoped vars for repeated rows

* Tests

* Fix merge
This commit is contained in:
Dominik Prokop 2023-10-23 11:46:35 +02:00 committed by GitHub
parent 2a7b6a533e
commit 120247667b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 350 additions and 94 deletions

View File

@ -3018,7 +3018,10 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"public/app/features/dashboard-scene/utils/test-utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -1,19 +1,8 @@
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
import {
EmbeddedScene,
SceneGridLayout,
SceneGridRow,
SceneTimeRange,
SceneVariableSet,
TestVariable,
VizPanel,
} from '@grafana/scenes';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
import { SceneGridLayout } from '@grafana/scenes';
import { activateFullSceneTree } from '../utils/test-utils';
import { PanelRepeaterGridItem, RepeatDirection } from './PanelRepeaterGridItem';
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
@ -22,7 +11,7 @@ setPluginImportUtils({
describe('PanelRepeaterGridItem', () => {
it('Given scene with variable with 2 values', async () => {
const { scene, repeater } = buildScene({ variableQueryTime: 0 });
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
activateFullSceneTree(scene);
@ -38,7 +27,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('Should wait for variable to load', async () => {
const { scene, repeater } = buildScene({ variableQueryTime: 1 });
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1 });
activateFullSceneTree(scene);
@ -50,7 +39,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('Should adjust container height to fit panels direction is horizontal', async () => {
const { scene, repeater } = buildScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
const layoutForceRender = jest.fn();
(scene.state.body as SceneGridLayout).forceRender = layoutForceRender;
@ -64,7 +53,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('Should adjust container height to fit panels when direction is vertical', async () => {
const { scene, repeater } = buildScene({ variableQueryTime: 0, itemHeight: 10, repeatDirection: 'v' });
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, itemHeight: 10, repeatDirection: 'v' });
activateFullSceneTree(scene);
@ -73,7 +62,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('Should adjust itemHeight when container is resized, direction horizontal', async () => {
const { scene, repeater } = buildScene({
const { scene, repeater } = buildPanelRepeaterScene({
variableQueryTime: 0,
itemHeight: 10,
repeatDirection: 'h',
@ -92,7 +81,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('Should adjust itemHeight when container is resized, direction vertical', async () => {
const { scene, repeater } = buildScene({
const { scene, repeater } = buildPanelRepeaterScene({
variableQueryTime: 0,
itemHeight: 10,
repeatDirection: 'v',
@ -110,7 +99,7 @@ describe('PanelRepeaterGridItem', () => {
});
it('When updating variable should update repeats', async () => {
const { scene, repeater, variable } = buildScene({ variableQueryTime: 0 });
const { scene, repeater, variable } = buildPanelRepeaterScene({ variableQueryTime: 0 });
activateFullSceneTree(scene);
@ -119,57 +108,3 @@ describe('PanelRepeaterGridItem', () => {
expect(repeater.state.repeatedPanels?.length).toBe(2);
});
});
interface SceneOptions {
variableQueryTime: number;
maxPerRow?: number;
itemHeight?: number;
repeatDirection?: RepeatDirection;
}
function buildScene(options: SceneOptions) {
const repeater = new PanelRepeaterGridItem({
variableName: 'server',
repeatedPanels: [],
repeatDirection: options.repeatDirection,
maxPerRow: options.maxPerRow,
itemHeight: options.itemHeight,
source: new VizPanel({
title: 'Panel $server',
pluginId: 'timeseries',
}),
});
const variable = 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' },
],
});
const scene = new EmbeddedScene({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
$variables: new SceneVariableSet({
variables: [variable],
}),
body: new SceneGridLayout({
children: [
new SceneGridRow({
children: [repeater],
}),
],
}),
});
return { scene, repeater, variable };
}

View File

@ -153,7 +153,7 @@ export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItem
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
}
private getMaxPerRow(): number {
public getMaxPerRow(): number {
return this.state.maxPerRow ?? 4;
}

View File

@ -374,7 +374,6 @@ export function buildGridItemForPanel(panel: PanelModel): SceneGridItemLike {
if (panel.repeat) {
const repeatDirection = panel.repeatDirection ?? 'h';
return new PanelRepeaterGridItem({
key: `grid-item-${panel.id}`,
x: panel.gridPos.x,

View File

@ -31,7 +31,7 @@ import { reduceTransformRegistryItem } from 'app/features/transformers/editors/R
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { activateFullSceneTree } from '../utils/test-utils';
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
import { getVizPanelKeyForPanelId } from '../utils/utils';
import { GRAFANA_DATASOURCE_REF } from './const';
@ -44,7 +44,13 @@ import {
buildGridItemForPanel,
transformSaveModelToScene,
} from './transformSaveModelToScene';
import { gridItemToPanel, transformSceneToSaveModel, trimDashboardForSnapshot } from './transformSceneToSaveModel';
import {
gridItemToPanel,
gridRowToSaveModel,
panelRepeaterToPanels,
transformSceneToSaveModel,
trimDashboardForSnapshot,
} from './transformSceneToSaveModel';
standardTransformersRegistry.setInit(() => [reduceTransformRegistryItem]);
setPluginImportUtils({
@ -597,6 +603,107 @@ describe('transformSceneToSaveModel', () => {
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 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;

View File

@ -1,4 +1,4 @@
import { isEmptyObject, TimeRange } from '@grafana/data';
import { isEmptyObject, ScopedVars, TimeRange } from '@grafana/data';
import {
SceneDataLayers,
SceneGridItem,
@ -10,6 +10,7 @@ import {
SceneDataTransformer,
SceneVariableSet,
AdHocFilterSet,
LocalValueVariable,
} from '@grafana/scenes';
import {
AnnotationQuery,
@ -45,7 +46,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
const data = state.$data;
const variablesSet = state.$variables;
const body = state.body;
const panels: Panel[] = [];
let panels: Panel[] = [];
let variables: VariableModel[] = [];
@ -55,9 +56,13 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
panels.push(gridItemToPanel(child, isSnapshot));
}
if (child instanceof PanelRepeaterGridItem) {
panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
}
if (child instanceof SceneGridRow) {
// Skip repeat clones
if (child.state.key!.indexOf('-clone-') > 0) {
// Skip repeat clones or when generating a snapshot
if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) {
continue;
}
gridRowToSaveModel(child, panels, isSnapshot);
@ -151,7 +156,6 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
if (gridItem instanceof PanelRepeaterGridItem) {
vizPanel = gridItem.state.source;
x = gridItem.state.x ?? 0;
y = gridItem.state.y ?? 0;
w = gridItem.state.width ?? 0;
@ -171,6 +175,7 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: vizPanel.state.displayMode === 'transparent',
...vizPanelDataToPanel(vizPanel, isSnapshot),
};
const panelTime = vizPanel.state.$timeRange;
@ -181,8 +186,22 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
panel.hideTimeOverride = panelTime.state.hideTimeOverride;
}
if (gridItem instanceof PanelRepeaterGridItem) {
panel.repeat = gridItem.state.variableName;
panel.maxPerRow = gridItem.state.maxPerRow;
panel.repeatDirection = gridItem.getRepeatDirection();
}
return panel;
}
function vizPanelDataToPanel(
vizPanel: VizPanel,
isSnapshot = false
): Pick<Panel, 'datasource' | 'targets' | 'maxDataPoints' | 'transformations'> {
const dataProvider = vizPanel.state.$data;
const panel: Pick<Panel, 'datasource' | 'targets' | 'maxDataPoints' | 'transformations'> = {};
// Dashboard datasource handling
if (dataProvider instanceof ShareQueryDataProvider) {
panel.datasource = {
@ -252,16 +271,64 @@ export function gridItemToPanel(gridItem: SceneGridItemLike, isSnapshot = false)
];
}
if (gridItem instanceof PanelRepeaterGridItem) {
panel.repeat = gridItem.state.variableName;
panel.maxPerRow = gridItem.state.maxPerRow;
panel.repeatDirection = gridItem.getRepeatDirection();
}
return panel;
}
export function panelRepeaterToPanels(repeater: PanelRepeaterGridItem, isSnapshot = false): Panel[] {
if (!isSnapshot) {
return [gridItemToPanel(repeater)];
} else {
if (repeater.state.repeatedPanels) {
const itemHeight = repeater.state.itemHeight ?? 10;
const rowCount = Math.ceil(repeater.state.repeatedPanels!.length / repeater.getMaxPerRow());
const columnCount = Math.ceil(repeater.state.repeatedPanels!.length / rowCount);
const w = 24 / columnCount;
const h = itemHeight;
const panels = repeater.state.repeatedPanels!.map((panel, index) => {
let x = 0,
y = 0;
if (repeater.state.repeatDirection === 'v') {
x = repeater.state.x!;
y = index * h;
} else {
x = (index % columnCount) * w;
y = repeater.state.y! + Math.floor(index / columnCount) * h;
}
const gridPos = { x, y, w, h };
const localVariable = panel.state.$variables!.getByName(repeater.state.variableName!) as LocalValueVariable;
const result: Panel = {
id: getPanelIdForVizPanel(panel),
type: panel.state.pluginId,
title: panel.state.title,
gridPos,
options: panel.state.options,
fieldConfig: (panel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
transformations: [],
transparent: panel.state.displayMode === 'transparent',
// @ts-expect-error scopedVars are runtime only properties, not part of the persisted Dashboardmodel
scopedVars: {
[repeater.state.variableName!]: {
text: localVariable?.state.text,
value: localVariable?.state.value,
},
},
...vizPanelDataToPanel(panel, isSnapshot),
};
return result;
});
return panels;
}
return [];
}
}
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>, isSnapshot = false) {
const collapsed = Boolean(gridRow.state.isCollapsed);
const rowPanel: RowPanel = {
type: 'row',
id: getPanelIdForVizPanel(gridRow),
@ -272,21 +339,52 @@ export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Pan
w: gridRow.state.width ?? 24,
h: gridRow.state.height ?? 1,
},
collapsed: Boolean(gridRow.state.isCollapsed),
collapsed,
panels: [],
};
if (gridRow.state.$behaviors?.length) {
const behavior = gridRow.state.$behaviors[0];
if (behavior instanceof RowRepeaterBehavior) {
rowPanel.repeat = behavior.state.variableName;
}
}
if (isSnapshot) {
// Rows that are repeated has SceneVariableSet attached to them.
if (gridRow.state.$variables) {
const localVariable = gridRow.state.$variables;
const scopedVars: ScopedVars = (localVariable.state.variables as LocalValueVariable[]).reduce((acc, variable) => {
return {
...acc,
[variable.state.name]: {
text: variable.state.text,
value: variable.state.value,
},
};
}, {});
// @ts-expect-error
rowPanel.scopedVars = scopedVars;
}
}
panelsArray.push(rowPanel);
const panelsInsideRow = gridRow.state.children.map((c) => gridItemToPanel(c, isSnapshot));
let panelsInsideRow: Panel[] = [];
if (isSnapshot) {
gridRow.state.children.forEach((c) => {
if (c instanceof PanelRepeaterGridItem) {
// Perform snapshot only for uncollapsed rows
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, !collapsed));
} else {
// Perform snapshot only for uncollapsed panels
panelsInsideRow.push(gridItemToPanel(c, !collapsed));
}
});
} else {
panelsInsideRow = gridRow.state.children.map((c) => gridItemToPanel(c));
}
if (gridRow.state.isCollapsed) {
rowPanel.panels = panelsInsideRow;

View File

@ -1,7 +1,23 @@
import { DeepPartial, SceneDeactivationHandler, SceneObject } from '@grafana/scenes';
import {
DeepPartial,
EmbeddedScene,
SceneDeactivationHandler,
SceneGridItem,
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';
import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem';
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>) {
const loadDashboardMock = jest.fn().mockResolvedValue(rsp);
setDashboardLoaderSrv({
@ -69,3 +85,101 @@ export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHand
}
};
}
interface SceneOptions {
variableQueryTime: number;
maxPerRow?: number;
itemHeight?: number;
repeatDirection?: RepeatDirection;
x?: number;
y?: number;
numberOfOptions?: number;
usePanelRepeater?: boolean;
useRowRepeater?: boolean;
}
export function buildPanelRepeaterScene(options: SceneOptions) {
const defaults = { usePanelRepeater: true, ...options };
const repeater = new PanelRepeaterGridItem({
variableName: 'server',
repeatedPanels: [],
repeatDirection: options.repeatDirection,
maxPerRow: options.maxPerRow,
itemHeight: options.itemHeight,
source: new VizPanel({
title: 'Panel $server',
pluginId: 'timeseries',
}),
x: options.x || 0,
y: options.y || 0,
});
const gridItem = new SceneGridItem({
x: 0,
y: 0,
width: 10,
height: 10,
body: new VizPanel({ title: 'Panel $server', pluginId: 'timeseries' }),
});
const rowChildren = defaults.usePanelRepeater ? repeater : gridItem;
const row = new SceneGridRow({
$behaviors: defaults.useRowRepeater
? [
new RowRepeaterBehavior({
variableName: 'handler',
sources: [rowChildren],
}),
]
: [],
children: defaults.useRowRepeater ? [] : [rowChildren],
});
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),
});
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),
});
const scene = new EmbeddedScene({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
$variables: new SceneVariableSet({
variables: [panelRepeatVariable, rowRepeatVariable],
}),
body: new SceneGridLayout({
children: [row],
}),
});
return { scene, repeater, row, variable: panelRepeatVariable };
}