diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts index 232e3e62b1b..85bd30efe59 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.gen.ts @@ -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 => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 0a702f9f884..f324bfbcca8 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -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: { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts index 79f701ee1be..c980cff7c45 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/examples.ts @@ -198,6 +198,11 @@ export const handyTestingSchema: DashboardV2Spec = { width: 200, x: 0, y: 0, + repeat: { + mode: 'variable', + value: 'customVar', + maxPerRow: 3, + }, }, }, ], diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index fb1619ba497..7bf5db88fe6 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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}`); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index 21b463f262f..e14e778650f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -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; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 605cb307c82..319886ffb75 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -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 { ); } +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; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 3a26fc19170..55a9c16cdd2 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -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