Dashboard SchemaV2: Panel repeater (#98654)

* Wip: working layout for repeaters

* Update schema

* only persist orig panel

* Keep only supported mode and rename repeater function

* refactor dimension calcs

* v1 transformer uses calculateGridItemDimensions
This commit is contained in:
Haris Rozajac 2025-01-10 08:09:06 -07:00 committed by GitHub
parent 7499611129
commit f3d2313f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 140 additions and 15 deletions

View File

@ -648,6 +648,21 @@ export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({
fiscalYearStartMonth: 0,
});
// other repeat modes will be added in the future: label, frame
export const RepeatMode = "variable";
export interface RepeatOptions {
mode: "variable";
value: string;
direction?: "h" | "v";
maxPerRow?: number;
}
export const defaultRepeatOptions = (): RepeatOptions => ({
mode: RepeatMode,
value: "",
});
export interface GridLayoutItemSpec {
x: number;
y: number;
@ -655,6 +670,7 @@ export interface GridLayoutItemSpec {
height: number;
// reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference
element: ElementReference;
repeat?: RepeatOptions;
}
export const defaultGridLayoutItemSpec = (): GridLayoutItemSpec => ({

View File

@ -455,12 +455,22 @@ TimeSettingsSpec: {
nowDelay?: string // v1: timepicker.nowDelay
}
RepeatMode: "variable" // other repeat modes will be added in the future: label, frame
RepeatOptions: {
mode: RepeatMode
value: string
direction?: "h" | "v"
maxPerRow?: int
}
GridLayoutItemSpec: {
x: int
y: int
width: int
height: int
element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference
repeat?: RepeatOptions
}
GridLayoutItemKind: {

View File

@ -198,6 +198,11 @@ export const handyTestingSchema: DashboardV2Spec = {
width: 200,
x: 0,
y: 0,
repeat: {
mode: 'variable',
value: 'customVar',
maxPerRow: 3,
},
},
},
],

View File

@ -245,10 +245,13 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte
key: `grid-item-${panel.spec.id}`,
x: element.spec.x,
y: element.spec.y,
width: element.spec.width,
width: element.spec.repeat?.direction === 'h' ? 24 : element.spec.width,
height: element.spec.height,
itemHeight: element.spec.height,
body: vizPanel,
variableName: element.spec.repeat?.value,
repeatDirection: element.spec.repeat?.direction,
maxPerRow: element.spec.repeat?.maxPerRow,
});
} else {
throw new Error(`Unknown element kind: ${element.kind}`);

View File

@ -37,7 +37,13 @@ import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getLibraryPanelBehavior, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import {
calculateGridItemDimensions,
getLibraryPanelBehavior,
getPanelIdForVizPanel,
getQueryRunnerFor,
isLibraryPanel,
} from '../utils/utils';
import { GRAFANA_DATASOURCE_REF } from './const';
import { dataLayersToAnnotations } from './dataLayersToAnnotations';
@ -319,11 +325,7 @@ export function panelRepeaterToPanels(repeater: DashboardGridItem, isSnapshot =
}
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 { h, w, columnCount } = calculateGridItemDimensions(repeater);
const panels = repeater.state.repeatedPanels!.map((panel, index) => {
let x = 0,
y = 0;

View File

@ -36,6 +36,7 @@ import {
AdhocVariableKind,
AnnotationQueryKind,
DataLink,
RepeatOptions,
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
@ -43,7 +44,13 @@ import { PanelTimeRange } from '../scene/PanelTimeRange';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getPanelIdForVizPanel, getQueryRunnerFor, getVizPanelKeyForPanelId } from '../utils/utils';
import {
getPanelIdForVizPanel,
getQueryRunnerFor,
getVizPanelKeyForPanelId,
isLibraryPanel,
calculateGridItemDimensions,
} from '../utils/utils';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { transformCursorSynctoEnum } from './transformToV2TypesUtils';
@ -107,7 +114,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
layout: {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(oldDash),
items: getGridLayoutItems(oldDash, isSnapshot),
},
},
// EOF layout
@ -142,16 +149,16 @@ function getLiveNow(state: DashboardSceneState) {
function getGridLayoutItems(state: DashboardSceneState, isSnapshot?: boolean): GridLayoutItemKind[] {
const body = state.body;
const elements: GridLayoutItemKind[] = [];
let elements: GridLayoutItemKind[] = [];
if (body instanceof DefaultGridLayoutManager) {
for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) {
// TODO: handle panel repeater scenario
// if (child.state.variableName) {
// panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
// } else {
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
// }
if (child.state.variableName) {
elements = elements.concat(repeaterToLayoutItems(child, isSnapshot));
} else {
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
}
}
// TODO: OLD transformer code
@ -164,6 +171,7 @@ function getGridLayoutItems(state: DashboardSceneState, isSnapshot?: boolean): G
// }
}
}
return elements;
}
@ -185,6 +193,7 @@ export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnap
x = gridItem_.state.x ?? 0;
y = gridItem_.state.y ?? 0;
width = gridItem_.state.width ?? 0;
const repeatVar = gridItem_.state.variableName;
// FIXME: which name should we use for the element reference, key or something else ?
const elementName = gridItem_.state.body.state.key ?? 'DefaultName';
@ -202,6 +211,23 @@ export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, isSnap
},
};
if (repeatVar) {
const repeat: RepeatOptions = {
mode: 'variable',
value: repeatVar,
};
if (gridItem_.state.maxPerRow) {
repeat.maxPerRow = gridItem_.getMaxPerRow();
}
if (gridItem_.state.repeatDirection) {
repeat.direction = gridItem_.getRepeatDirection();
}
elementGridItem.spec.repeat = repeat;
}
if (!elementGridItem) {
throw new Error('Unsupported grid item type');
}
@ -367,6 +393,60 @@ function createElements(panels: PanelKind[]): Record<string, PanelKind> {
);
}
function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] {
if (!isSnapshot) {
return [gridItemToGridLayoutItemKind(repeater)];
} else {
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
// TODO: implement
// const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
// return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
return [];
}
if (repeater.state.repeatedPanels) {
const { h, w, columnCount } = calculateGridItemDimensions(repeater);
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 result: GridLayoutItemKind = {
kind: 'GridLayoutItem',
spec: {
x: gridPos.x,
y: gridPos.y,
width: gridPos.w,
height: gridPos.h,
repeat: {
mode: 'variable',
value: repeater.state.variableName!,
maxPerRow: repeater.getMaxPerRow(),
direction: repeater.state.repeatDirection,
},
element: {
kind: 'ElementReference',
name: panel.state.key!,
},
},
};
return result;
});
return panels;
}
return [];
}
}
function getVariables(oldDash: DashboardSceneState) {
const variablesSet = oldDash.$variables;

View File

@ -18,6 +18,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types';
export const NEW_PANEL_HEIGHT = 8;
@ -250,6 +251,14 @@ export function getLibraryPanelBehavior(vizPanel: VizPanel): LibraryPanelBehavio
return undefined;
}
export function calculateGridItemDimensions(repeater: DashboardGridItem) {
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 = repeater.state.itemHeight ?? 10;
return { h, w, columnCount };
}
/**
* Activates any inactive ancestors of the scene object.
* Useful when rendering a scene object out of context of it's parent