import { config } from '@grafana/runtime'; import { SceneGridItemLike, SceneGridLayout, SceneGridRow, SceneObject, VizPanel, VizPanelMenu, VizPanelState, } from '@grafana/scenes'; import { DashboardV2Spec, GridLayoutItemKind, GridLayoutKind, GridLayoutRowKind, RepeatOptions, Element, GridLayoutItemSpec, PanelKind, LibraryPanelKind, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { contextSrv } from 'app/core/core'; import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { PanelNotices } from '../../scene/PanelNotices'; import { AngularDeprecation } from '../../scene/angular/AngularDeprecation'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../../scene/layout-default/RowRepeaterBehavior'; import { RowActions } from '../../scene/layout-default/row-actions/RowActions'; import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext'; import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager'; import { getOriginalKey, isClonedKey } from '../../utils/clone'; import { calculateGridItemDimensions, getPanelIdForVizPanel, getVizPanelKeyForPanelId, isLibraryPanel, } from '../../utils/utils'; import { GRID_ROW_HEIGHT } from '../const'; import { buildVizPanel } from './utils'; export class DefaultGridLayoutManagerSerializer implements LayoutManagerSerializer { serialize(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'] { return { kind: 'GridLayout', spec: { items: getGridLayoutItems(layoutManager, isSnapshot), }, }; } deserialize( layout: DashboardV2Spec['layout'], elements: DashboardV2Spec['elements'], preload: boolean ): DashboardLayoutManager { if (layout.kind !== 'GridLayout') { throw new Error('Invalid layout kind'); } return new DefaultGridLayoutManager({ grid: new SceneGridLayout({ isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'), children: createSceneGridLayoutForItems(layout, elements), }), }); } } function getGridLayoutItems( body: DefaultGridLayoutManager, isSnapshot?: boolean ): Array { let items: Array = []; for (const child of body.state.grid.state.children) { if (child instanceof DashboardGridItem) { // TODO: handle panel repeater scenario if (child.state.variableName) { items = items.concat(repeaterToLayoutItems(child, isSnapshot)); } else { items.push(gridItemToGridLayoutItemKind(child)); } } else if (child instanceof SceneGridRow) { if (isClonedKey(child.state.key!) && !isSnapshot) { // Skip repeat rows continue; } items.push(gridRowToLayoutRowKind(child, isSnapshot)); } } return items; } function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined { if (row.state.$behaviors) { for (const behavior of row.state.$behaviors) { if (behavior instanceof RowRepeaterBehavior) { return { value: behavior.state.variableName, mode: 'variable' }; } } } return undefined; } function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind { const children = row.state.children.map((child) => { if (!(child instanceof DashboardGridItem)) { throw new Error('Unsupported row child type'); } const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT; return gridItemToGridLayoutItemKind(child, y); }); return { kind: 'GridLayoutRow', spec: { title: row.state.title, y: row.state.y ?? 0, collapsed: Boolean(row.state.isCollapsed), elements: children, repeat: getRowRepeat(row), }, }; } function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind { let elementGridItem: GridLayoutItemKind | undefined; let x = 0, y = 0, width = 0, height = 0; let gridItem_ = gridItem; if (!(gridItem_.state.body instanceof VizPanel)) { throw new Error('DashboardGridItem body expected to be VizPanel'); } // Get the grid position and size height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0; 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 = getVizPanelKeyForPanelId(getPanelIdForVizPanel(gridItem_.state.body)); elementGridItem = { kind: 'GridLayoutItem', spec: { x, y: yOverride ?? y, width: width, height: height, element: { kind: 'ElementReference', name: elementName, }, }, }; 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'); } return elementGridItem; } 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 createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record): SceneGridItemLike[] { const gridElements = layout.spec.items; return gridElements.map((element) => { if (element.kind === 'GridLayoutItem') { const panel = elements[element.spec.element.name]; if (!panel) { throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`); } if (panel.kind === 'Panel') { return buildGridItem(element.spec, panel); } else if (panel.kind === 'LibraryPanel') { const libraryPanel = buildLibraryPanel(panel); return new DashboardGridItem({ key: `grid-item-${panel.spec.id}`, x: element.spec.x, y: element.spec.y, width: element.spec.width, height: element.spec.height, itemHeight: element.spec.height, body: libraryPanel, }); } else { throw new Error(`Unknown element kind: ${element.kind}`); } } else if (element.kind === 'GridLayoutRow') { const children = element.spec.elements.map((gridElement) => { const panel = elements[getOriginalKey(gridElement.spec.element.name)]; if (panel.kind === 'Panel') { return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y); } else { throw new Error(`Unknown element kind: ${gridElement.kind}`); } }); let behaviors: SceneObject[] | undefined; if (element.spec.repeat) { behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })]; } return new SceneGridRow({ y: element.spec.y, isCollapsed: element.spec.collapsed, title: element.spec.title, $behaviors: behaviors, actions: new RowActions({}), children, }); } else { // If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error. //@ts-expect-error throw new Error(`Unknown layout element kind: ${element.kind}`); } }); } function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride?: number): DashboardGridItem { const vizPanel = buildVizPanel(panel); return new DashboardGridItem({ key: `grid-item-${panel.spec.id}`, x: gridItem.x, y: yOverride ?? gridItem.y, width: gridItem.repeat?.direction === 'h' ? 24 : gridItem.width, height: gridItem.height, itemHeight: gridItem.height, body: vizPanel, variableName: gridItem.repeat?.value, repeatDirection: gridItem.repeat?.direction, maxPerRow: gridItem.repeat?.maxPerRow, }); } function buildLibraryPanel(panel: LibraryPanelKind): VizPanel { const titleItems: SceneObject[] = []; if (config.featureToggles.angularDeprecationUI) { titleItems.push(new AngularDeprecation()); } titleItems.push( new VizPanelLinks({ rawLinks: [], menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }), }) ); titleItems.push(new PanelNotices()); const vizPanelState: VizPanelState = { key: getVizPanelKeyForPanelId(panel.spec.id), titleItems, $behaviors: [ new LibraryPanelBehavior({ uid: panel.spec.libraryPanel.uid, name: panel.spec.libraryPanel.name, }), ], extendPanelContext: setDashboardPanelContext, pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, title: panel.spec.title, options: {}, fieldConfig: { defaults: {}, overrides: [], }, }; if (!config.publicDashboardAccessToken) { vizPanelState.menu = new VizPanelMenu({ $behaviors: [panelMenuBehavior], }); } return new VizPanel(vizPanelState); }