mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Save rows and responsive grid layouts in v2 (#100035)
* save rows and responsive grid layouts in v2 * Add back accidentally removed assert * adress feedback * use getGridItemKeyForPanel * Fix import * fix another import * Remove RowGridLayout
This commit is contained in:
parent
882b993496
commit
a412394a14
@ -43,7 +43,7 @@ DashboardV2Spec: {
|
||||
|
||||
annotations: [...AnnotationQueryKind]
|
||||
|
||||
layout: GridLayoutKind
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind
|
||||
|
||||
|
||||
// Plugins only. The version of the dashboard installed together with the plugin.
|
||||
@ -488,6 +488,11 @@ RowRepeatOptions: {
|
||||
value: string
|
||||
}
|
||||
|
||||
ResponsiveGridRepeatOptions: {
|
||||
mode: RepeatMode
|
||||
value: string
|
||||
}
|
||||
|
||||
GridLayoutItemSpec: {
|
||||
x: int
|
||||
y: int
|
||||
@ -524,6 +529,47 @@ GridLayoutKind: {
|
||||
spec: GridLayoutSpec
|
||||
}
|
||||
|
||||
RowsLayoutKind: {
|
||||
kind: "RowsLayout"
|
||||
spec: RowsLayoutSpec
|
||||
}
|
||||
|
||||
RowsLayoutSpec: {
|
||||
rows: [...RowsLayoutRowKind]
|
||||
}
|
||||
|
||||
RowsLayoutRowKind: {
|
||||
kind: "RowsLayoutRow"
|
||||
spec: RowsLayoutRowSpec
|
||||
}
|
||||
|
||||
RowsLayoutRowSpec: {
|
||||
title?: string
|
||||
collapsed: bool
|
||||
repeat?: RowRepeatOptions
|
||||
layout: GridLayoutKind | ResponsiveGridLayoutKind
|
||||
}
|
||||
|
||||
ResponsiveGridLayoutKind: {
|
||||
kind: "ResponsiveGridLayout"
|
||||
spec: ResponsiveGridLayoutSpec
|
||||
}
|
||||
|
||||
ResponsiveGridLayoutSpec: {
|
||||
row: string,
|
||||
col: string,
|
||||
items: [...ResponsiveGridLayoutItemKind]
|
||||
}
|
||||
|
||||
ResponsiveGridLayoutItemKind: {
|
||||
kind: "ResponsiveGridLayoutItem"
|
||||
spec: ResponsiveGridLayoutItemSpec
|
||||
}
|
||||
|
||||
ResponsiveGridLayoutItemSpec: {
|
||||
element: ElementReference
|
||||
}
|
||||
|
||||
PanelSpec: {
|
||||
id: number
|
||||
title: string
|
||||
|
@ -30,7 +30,7 @@ export interface DashboardV2Spec {
|
||||
variables: VariableKind[];
|
||||
elements: Record<string, Element>;
|
||||
annotations: AnnotationQueryKind[];
|
||||
layout: GridLayoutKind;
|
||||
layout: GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind;
|
||||
// Plugins only. The version of the dashboard installed together with the plugin.
|
||||
// This is used to determine if the dashboard should be updated when the plugin is updated.
|
||||
revision?: number;
|
||||
@ -710,6 +710,16 @@ export const defaultRowRepeatOptions = (): RowRepeatOptions => ({
|
||||
value: "",
|
||||
});
|
||||
|
||||
export interface ResponsiveGridRepeatOptions {
|
||||
mode: "variable";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const defaultResponsiveGridRepeatOptions = (): ResponsiveGridRepeatOptions => ({
|
||||
mode: RepeatMode,
|
||||
value: "",
|
||||
});
|
||||
|
||||
export interface GridLayoutItemSpec {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -752,6 +762,7 @@ export interface GridLayoutRowSpec {
|
||||
y: number;
|
||||
collapsed: boolean;
|
||||
title: string;
|
||||
// Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard.
|
||||
elements: GridLayoutItemKind[];
|
||||
repeat?: RowRepeatOptions;
|
||||
}
|
||||
@ -781,6 +792,86 @@ export const defaultGridLayoutKind = (): GridLayoutKind => ({
|
||||
spec: defaultGridLayoutSpec(),
|
||||
});
|
||||
|
||||
export interface RowsLayoutKind {
|
||||
kind: "RowsLayout";
|
||||
spec: RowsLayoutSpec;
|
||||
}
|
||||
|
||||
export const defaultRowsLayoutKind = (): RowsLayoutKind => ({
|
||||
kind: "RowsLayout",
|
||||
spec: defaultRowsLayoutSpec(),
|
||||
});
|
||||
|
||||
export interface RowsLayoutSpec {
|
||||
rows: RowsLayoutRowKind[];
|
||||
}
|
||||
|
||||
export const defaultRowsLayoutSpec = (): RowsLayoutSpec => ({
|
||||
rows: [],
|
||||
});
|
||||
|
||||
export interface RowsLayoutRowKind {
|
||||
kind: "RowsLayoutRow";
|
||||
spec: RowsLayoutRowSpec;
|
||||
}
|
||||
|
||||
export const defaultRowsLayoutRowKind = (): RowsLayoutRowKind => ({
|
||||
kind: "RowsLayoutRow",
|
||||
spec: defaultRowsLayoutRowSpec(),
|
||||
});
|
||||
|
||||
export interface RowsLayoutRowSpec {
|
||||
title?: string;
|
||||
collapsed: boolean;
|
||||
repeat?: RowRepeatOptions;
|
||||
layout: GridLayoutKind | ResponsiveGridLayoutKind;
|
||||
}
|
||||
|
||||
export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({
|
||||
collapsed: false,
|
||||
layout: defaultGridLayoutKind(),
|
||||
});
|
||||
|
||||
export interface ResponsiveGridLayoutKind {
|
||||
kind: "ResponsiveGridLayout";
|
||||
spec: ResponsiveGridLayoutSpec;
|
||||
}
|
||||
|
||||
export const defaultResponsiveGridLayoutKind = (): ResponsiveGridLayoutKind => ({
|
||||
kind: "ResponsiveGridLayout",
|
||||
spec: defaultResponsiveGridLayoutSpec(),
|
||||
});
|
||||
|
||||
export interface ResponsiveGridLayoutSpec {
|
||||
row: string;
|
||||
col: string;
|
||||
items: ResponsiveGridLayoutItemKind[];
|
||||
}
|
||||
|
||||
export const defaultResponsiveGridLayoutSpec = (): ResponsiveGridLayoutSpec => ({
|
||||
row: "",
|
||||
col: "",
|
||||
items: [],
|
||||
});
|
||||
|
||||
export interface ResponsiveGridLayoutItemKind {
|
||||
kind: "ResponsiveGridLayoutItem";
|
||||
spec: ResponsiveGridLayoutItemSpec;
|
||||
}
|
||||
|
||||
export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemKind => ({
|
||||
kind: "ResponsiveGridLayoutItem",
|
||||
spec: defaultResponsiveGridLayoutItemSpec(),
|
||||
});
|
||||
|
||||
export interface ResponsiveGridLayoutItemSpec {
|
||||
element: ElementReference;
|
||||
}
|
||||
|
||||
export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({
|
||||
element: defaultElementReference(),
|
||||
});
|
||||
|
||||
export interface PanelSpec {
|
||||
id: number;
|
||||
title: string;
|
||||
|
@ -35,6 +35,11 @@ export class ResponsiveGridLayoutManager
|
||||
|
||||
public readonly descriptor = ResponsiveGridLayoutManager.descriptor;
|
||||
|
||||
public static defaultCSS = {
|
||||
templateColumns: 'repeat(auto-fit, minmax(400px, auto))',
|
||||
autoRows: 'minmax(300px, auto)',
|
||||
};
|
||||
|
||||
public constructor(state: ResponsiveGridLayoutManagerState) {
|
||||
super(state);
|
||||
|
||||
@ -118,8 +123,8 @@ export class ResponsiveGridLayoutManager
|
||||
return new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
children: [],
|
||||
templateColumns: 'repeat(auto-fit, minmax(400px, auto))',
|
||||
autoRows: 'minmax(300px, auto)',
|
||||
templateColumns: ResponsiveGridLayoutManager.defaultCSS.templateColumns,
|
||||
autoRows: ResponsiveGridLayoutManager.defaultCSS.autoRows,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
defaultDashboardV2Spec,
|
||||
defaultPanelSpec,
|
||||
defaultTimeSettingsSpec,
|
||||
GridLayoutKind,
|
||||
PanelSpec,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types';
|
||||
@ -736,7 +737,8 @@ describe('DashboardSceneSerializer', () => {
|
||||
const saveAsModel = serializer.getSaveAsModel(emptyDashboard, baseOptions);
|
||||
|
||||
expect(saveAsModel.elements).toEqual({});
|
||||
expect(saveAsModel.layout.spec.items).toEqual([]);
|
||||
expect(saveAsModel.layout.kind).toBe('GridLayout');
|
||||
expect((saveAsModel.layout as GridLayoutKind).spec.items).toEqual([]);
|
||||
expect(saveAsModel.variables).toEqual([]);
|
||||
});
|
||||
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
DashboardV2Spec,
|
||||
DatasourceVariableKind,
|
||||
GridLayoutItemSpec,
|
||||
GridLayoutSpec,
|
||||
GroupByVariableKind,
|
||||
IntervalVariableKind,
|
||||
QueryVariableKind,
|
||||
@ -231,7 +232,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
const panel = getPanelElement(dash, 'panel-1')!;
|
||||
expect(layout.state.grid.state.children.length).toBe(3);
|
||||
expect(layout.state.grid.state.children[0].state.key).toBe(`grid-item-${panel.spec.id}`);
|
||||
const gridLayoutItemSpec = dash.layout.spec.items[0].spec as GridLayoutItemSpec;
|
||||
const gridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[0].spec as GridLayoutItemSpec;
|
||||
expect(layout.state.grid.state.children[0].state.width).toBe(gridLayoutItemSpec.width);
|
||||
expect(layout.state.grid.state.children[0].state.height).toBe(gridLayoutItemSpec.height);
|
||||
expect(layout.state.grid.state.children[0].state.x).toBe(gridLayoutItemSpec.x);
|
||||
@ -242,7 +243,7 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
// Library Panel
|
||||
const libraryPanel = getLibraryPanelElement(dash, 'panel-2')!;
|
||||
expect(layout.state.grid.state.children[1].state.key).toBe(`grid-item-${libraryPanel.spec.id}`);
|
||||
const libraryGridLayoutItemSpec = dash.layout.spec.items[1].spec as GridLayoutItemSpec;
|
||||
const libraryGridLayoutItemSpec = (dash.layout.spec as GridLayoutSpec).items[1].spec as GridLayoutItemSpec;
|
||||
expect(layout.state.grid.state.children[1].state.width).toBe(libraryGridLayoutItemSpec.width);
|
||||
expect(layout.state.grid.state.children[1].state.height).toBe(libraryGridLayoutItemSpec.height);
|
||||
expect(layout.state.grid.state.children[1].state.x).toBe(libraryGridLayoutItemSpec.x);
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
GroupByVariable,
|
||||
IntervalVariable,
|
||||
QueryVariable,
|
||||
SceneCSSGridLayout,
|
||||
SceneDataLayerControls,
|
||||
SceneDataProvider,
|
||||
SceneDataQuery,
|
||||
@ -46,6 +47,8 @@ import {
|
||||
defaultQueryVariableKind,
|
||||
defaultTextVariableKind,
|
||||
GridLayoutItemSpec,
|
||||
GridLayoutKind,
|
||||
Element,
|
||||
GroupByVariableKind,
|
||||
IntervalVariableKind,
|
||||
LibraryPanelKind,
|
||||
@ -53,6 +56,7 @@ import {
|
||||
PanelQueryKind,
|
||||
QueryVariableKind,
|
||||
TextVariableKind,
|
||||
ResponsiveGridLayoutItemKind,
|
||||
} from '@grafana/schema/src/schema/dashboard/v2alpha0';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import {
|
||||
@ -86,9 +90,14 @@ 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 { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
|
||||
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
|
||||
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
|
||||
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
|
||||
import { getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
import { getGridItemKeyForPanelId, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
import { GRID_ROW_HEIGHT } from './const';
|
||||
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
|
||||
@ -166,6 +175,8 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
meta.canSave = false;
|
||||
}
|
||||
|
||||
const layoutManager: DashboardLayoutManager = createLayoutManager(dashboard);
|
||||
|
||||
const dashboardScene = new DashboardScene({
|
||||
description: dashboard.description,
|
||||
editable: dashboard.editable,
|
||||
@ -178,12 +189,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
title: dashboard.title,
|
||||
uid: metadata.name,
|
||||
version: parseInt(metadata.resourceVersion, 10),
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
isLazy: !(dashboard.preload || contextSrv.user.authenticatedBy === 'render'),
|
||||
children: createSceneGridLayoutForItems(dashboard),
|
||||
}),
|
||||
}),
|
||||
body: layoutManager,
|
||||
$timeRange: new SceneTimeRange({
|
||||
from: dashboard.timeSettings.from,
|
||||
to: dashboard.timeSettings.to,
|
||||
@ -249,12 +255,86 @@ function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride
|
||||
});
|
||||
}
|
||||
|
||||
function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridItemLike[] {
|
||||
const gridElements = dashboard.layout.spec.items;
|
||||
function createLayoutManager(dashboard: DashboardV2Spec): DashboardLayoutManager {
|
||||
if (dashboard.layout.kind === 'GridLayout') {
|
||||
return new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
isLazy: !(dashboard.preload || contextSrv.user.authenticatedBy === 'render'),
|
||||
children: createSceneGridLayoutForItems(dashboard.layout, dashboard.elements),
|
||||
}),
|
||||
});
|
||||
} else if (dashboard.layout.kind === 'RowsLayout') {
|
||||
return new RowsLayoutManager({
|
||||
rows: dashboard.layout.spec.rows.map((row) => {
|
||||
let layout: DashboardLayoutManager | undefined = undefined;
|
||||
|
||||
if (row.spec.layout.kind === 'GridLayout') {
|
||||
layout = new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: createSceneGridLayoutForItems(row.spec.layout, dashboard.elements),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (row.spec.layout.kind === 'ResponsiveGridLayout') {
|
||||
layout = new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
templateColumns: row.spec.layout.spec.col,
|
||||
autoRows: row.spec.layout.spec.row,
|
||||
children: createResponsiveGridItems(row.spec.layout.spec.items, dashboard.elements),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (!layout) {
|
||||
throw new Error(`Unsupported layout kind: ${row.spec.layout.kind} in row`);
|
||||
}
|
||||
return new RowItem({
|
||||
title: row.spec.title,
|
||||
isCollapsed: row.spec.collapsed,
|
||||
layout: layout,
|
||||
});
|
||||
}),
|
||||
});
|
||||
} else if (dashboard.layout.kind === 'ResponsiveGridLayout') {
|
||||
return new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
templateColumns: dashboard.layout.spec.col,
|
||||
autoRows: dashboard.layout.spec.row,
|
||||
children: createResponsiveGridItems(dashboard.layout.spec.items, dashboard.elements),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore - this complains because we should never reach this point. If the model does not match the schema we will though.
|
||||
throw new Error(`Unsupported layout type: ${dashboard.layout.kind}`);
|
||||
}
|
||||
|
||||
function createResponsiveGridItems(
|
||||
items: ResponsiveGridLayoutItemKind[],
|
||||
elements: Record<string, Element>
|
||||
): ResponsiveGridItem[] {
|
||||
return items.map((item) => {
|
||||
const panel = elements[item.spec.element.name];
|
||||
if (!panel) {
|
||||
throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`);
|
||||
}
|
||||
if (panel.kind !== 'Panel') {
|
||||
throw new Error(`Unsupported element kind: ${panel.kind}`);
|
||||
}
|
||||
return new ResponsiveGridItem({
|
||||
key: getGridItemKeyForPanelId(panel.spec.id),
|
||||
body: buildVizPanel(panel),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<string, Element>): SceneGridItemLike[] {
|
||||
const gridElements = layout.spec.items;
|
||||
|
||||
return gridElements.map((element) => {
|
||||
if (element.kind === 'GridLayoutItem') {
|
||||
const panel = dashboard.elements[element.spec.element.name];
|
||||
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`);
|
||||
@ -279,7 +359,7 @@ function createSceneGridLayoutForItems(dashboard: DashboardV2Spec): SceneGridIte
|
||||
}
|
||||
} else if (element.kind === 'GridLayoutRow') {
|
||||
const children = element.spec.elements.map((gridElement) => {
|
||||
const panel = dashboard.elements[gridElement.spec.element.name];
|
||||
const panel = elements[gridElement.spec.element.name];
|
||||
if (panel.kind === 'Panel') {
|
||||
return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y);
|
||||
} else {
|
||||
|
@ -43,6 +43,10 @@ import {
|
||||
DashboardCursorSync,
|
||||
FieldConfig,
|
||||
FieldColor,
|
||||
GridLayoutKind,
|
||||
RowsLayoutKind,
|
||||
ResponsiveGridLayoutKind,
|
||||
ResponsiveGridLayoutItemKind,
|
||||
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
@ -50,6 +54,10 @@ import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
|
||||
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
|
||||
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||
import { isClonedKey } from '../utils/clone';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import {
|
||||
@ -73,22 +81,22 @@ type DeepPartial<T> = T extends object
|
||||
: T;
|
||||
|
||||
export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnapshot = false): DashboardV2Spec {
|
||||
const oldDash = scene.state;
|
||||
const timeRange = oldDash.$timeRange!.state;
|
||||
const sceneDash = scene.state;
|
||||
const timeRange = sceneDash.$timeRange!.state;
|
||||
|
||||
const controlsState = oldDash.controls?.state;
|
||||
const controlsState = sceneDash.controls?.state;
|
||||
const refreshPicker = controlsState?.refreshPicker;
|
||||
|
||||
const dashboardSchemaV2: DeepPartial<DashboardV2Spec> = {
|
||||
//dashboard settings
|
||||
title: oldDash.title,
|
||||
description: oldDash.description ?? '',
|
||||
cursorSync: getCursorSync(oldDash),
|
||||
liveNow: getLiveNow(oldDash),
|
||||
preload: oldDash.preload,
|
||||
editable: oldDash.editable,
|
||||
links: oldDash.links,
|
||||
tags: oldDash.tags,
|
||||
title: sceneDash.title,
|
||||
description: sceneDash.description ?? '',
|
||||
cursorSync: getCursorSync(sceneDash),
|
||||
liveNow: getLiveNow(sceneDash),
|
||||
preload: sceneDash.preload,
|
||||
editable: sceneDash.editable,
|
||||
links: sceneDash.links,
|
||||
tags: sceneDash.tags,
|
||||
// EOF dashboard settings
|
||||
|
||||
// time settings
|
||||
@ -107,24 +115,19 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
// EOF time settings
|
||||
|
||||
// variables
|
||||
variables: getVariables(oldDash),
|
||||
variables: getVariables(sceneDash),
|
||||
// EOF variables
|
||||
|
||||
// elements
|
||||
elements: getElements(oldDash),
|
||||
elements: getElements(sceneDash),
|
||||
// EOF elements
|
||||
|
||||
// annotations
|
||||
annotations: getAnnotations(oldDash),
|
||||
annotations: getAnnotations(sceneDash),
|
||||
// EOF annotations
|
||||
|
||||
// layout
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: getGridLayoutItems(oldDash, isSnapshot),
|
||||
},
|
||||
},
|
||||
layout: getLayout(sceneDash.body, isSnapshot),
|
||||
// EOF layout
|
||||
};
|
||||
|
||||
@ -141,6 +144,75 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
}
|
||||
}
|
||||
|
||||
function getLayout(
|
||||
layoutManager: DashboardLayoutManager,
|
||||
isSnapshot?: boolean
|
||||
): GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind {
|
||||
if (layoutManager instanceof DefaultGridLayoutManager) {
|
||||
return getGridLayout(layoutManager, isSnapshot);
|
||||
} else if (layoutManager instanceof RowsLayoutManager) {
|
||||
return {
|
||||
kind: 'RowsLayout',
|
||||
spec: {
|
||||
rows: layoutManager.state.rows.map((row) => {
|
||||
if (row.state.layout instanceof RowsLayoutManager) {
|
||||
throw new Error('Nesting row layouts is not supported');
|
||||
}
|
||||
let layout: GridLayoutKind | ResponsiveGridLayoutKind | undefined = undefined;
|
||||
if (row.state.layout instanceof DefaultGridLayoutManager) {
|
||||
layout = getGridLayout(row.state.layout, isSnapshot);
|
||||
} else if (row.state.layout instanceof ResponsiveGridLayoutManager) {
|
||||
layout = {
|
||||
kind: 'ResponsiveGridLayout',
|
||||
spec: {
|
||||
items: getResponsiveGridLayoutItems(row.state.layout),
|
||||
col:
|
||||
row.state.layout.state.layout.state.templateColumns?.toString() ??
|
||||
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
|
||||
row:
|
||||
row.state.layout.state.layout.state.autoRows?.toString() ??
|
||||
ResponsiveGridLayoutManager.defaultCSS.autoRows,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!layout) {
|
||||
throw new Error('Unsupported layout type');
|
||||
}
|
||||
return {
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: row.state.title,
|
||||
collapsed: row.state.isCollapsed ?? false,
|
||||
layout: layout,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
} else if (layoutManager instanceof ResponsiveGridLayoutManager) {
|
||||
return {
|
||||
kind: 'ResponsiveGridLayout',
|
||||
spec: {
|
||||
items: getResponsiveGridLayoutItems(layoutManager),
|
||||
col:
|
||||
layoutManager.state.layout.state.templateColumns?.toString() ??
|
||||
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
|
||||
row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('Unsupported layout type');
|
||||
}
|
||||
|
||||
function getGridLayout(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): GridLayoutKind {
|
||||
return {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: getGridLayoutItems(layoutManager, isSnapshot),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getCursorSync(state: DashboardSceneState) {
|
||||
const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state
|
||||
.sync;
|
||||
@ -160,33 +232,49 @@ function getLiveNow(state: DashboardSceneState) {
|
||||
}
|
||||
|
||||
function getGridLayoutItems(
|
||||
state: DashboardSceneState,
|
||||
body: DefaultGridLayoutManager,
|
||||
isSnapshot?: boolean
|
||||
): Array<GridLayoutItemKind | GridLayoutRowKind> {
|
||||
const body = state.body;
|
||||
let elements: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
|
||||
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) {
|
||||
elements = elements.concat(repeaterToLayoutItems(child, isSnapshot));
|
||||
} else {
|
||||
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
|
||||
}
|
||||
} else if (child instanceof SceneGridRow) {
|
||||
if (isClonedKey(child.state.key!) && !isSnapshot) {
|
||||
// Skip repeat rows
|
||||
continue;
|
||||
}
|
||||
elements.push(gridRowToLayoutRowKind(child, isSnapshot));
|
||||
for (const child of body.state.grid.state.children) {
|
||||
if (child instanceof DashboardGridItem) {
|
||||
// TODO: handle panel repeater scenario
|
||||
if (child.state.variableName) {
|
||||
elements = elements.concat(repeaterToLayoutItems(child, isSnapshot));
|
||||
} else {
|
||||
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
|
||||
}
|
||||
} else if (child instanceof SceneGridRow) {
|
||||
if (isClonedKey(child.state.key!) && !isSnapshot) {
|
||||
// Skip repeat rows
|
||||
continue;
|
||||
}
|
||||
elements.push(gridRowToLayoutRowKind(child, isSnapshot));
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
function getResponsiveGridLayoutItems(body: ResponsiveGridLayoutManager): ResponsiveGridLayoutItemKind[] {
|
||||
const items: ResponsiveGridLayoutItemKind[] = [];
|
||||
|
||||
for (const child of body.state.layout.state.children) {
|
||||
if (child instanceof ResponsiveGridItem) {
|
||||
items.push({
|
||||
kind: 'ResponsiveGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: child.state?.body?.state.key ?? 'DefaultName',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function gridItemToGridLayoutItemKind(
|
||||
gridItem: DashboardGridItem,
|
||||
isSnapshot = false,
|
||||
@ -729,15 +817,44 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
|
||||
if (!('layout' in dash) || typeof dash.layout !== 'object' || dash.layout === null) {
|
||||
throw new Error('Layout is not an object or is null');
|
||||
}
|
||||
if (!('kind' in dash.layout) || dash.layout.kind !== 'GridLayout') {
|
||||
throw new Error('Layout kind is not GridLayout');
|
||||
|
||||
if (!('kind' in dash.layout) || dash.layout.kind === 'GridLayout') {
|
||||
validateGridLayout(dash.layout);
|
||||
}
|
||||
if (!('spec' in dash.layout) || typeof dash.layout.spec !== 'object' || dash.layout.spec === null) {
|
||||
throw new Error('Layout spec is not an object or is null');
|
||||
}
|
||||
if (!('items' in dash.layout.spec) || !Array.isArray(dash.layout.spec.items)) {
|
||||
throw new Error('Layout spec items is not an array');
|
||||
|
||||
if (!('kind' in dash.layout) || dash.layout.kind === 'RowsLayout') {
|
||||
validateRowsLayout(dash.layout);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateGridLayout(layout: unknown) {
|
||||
if (typeof layout !== 'object' || layout === null) {
|
||||
throw new Error('Layout is not an object or is null');
|
||||
}
|
||||
if (!('kind' in layout) || layout.kind !== 'GridLayout') {
|
||||
throw new Error('Layout kind is not GridLayout');
|
||||
}
|
||||
if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
|
||||
throw new Error('Layout spec is not an object or is null');
|
||||
}
|
||||
if (!('items' in layout.spec) || !Array.isArray(layout.spec.items)) {
|
||||
throw new Error('Layout spec items is not an array');
|
||||
}
|
||||
}
|
||||
|
||||
function validateRowsLayout(layout: unknown) {
|
||||
if (typeof layout !== 'object' || layout === null) {
|
||||
throw new Error('Layout is not an object or is null');
|
||||
}
|
||||
if (!('kind' in layout) || layout.kind !== 'RowsLayout') {
|
||||
throw new Error('Layout kind is not RowsLayout');
|
||||
}
|
||||
if (!('spec' in layout) || typeof layout.spec !== 'object' || layout.spec === null) {
|
||||
throw new Error('Layout spec is not an object or is null');
|
||||
}
|
||||
if (!('rows' in layout.spec) || !Array.isArray(layout.spec.rows)) {
|
||||
throw new Error('Layout spec items is not an array');
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
DashboardV2Spec,
|
||||
GridLayoutItemKind,
|
||||
GridLayoutItemSpec,
|
||||
GridLayoutKind,
|
||||
GridLayoutRowSpec,
|
||||
PanelKind,
|
||||
VariableKind,
|
||||
@ -469,8 +470,10 @@ describe('ResponseTransformers', () => {
|
||||
expect(spec.annotations).toEqual([]);
|
||||
|
||||
// Panel
|
||||
expect(spec.layout.spec.items).toHaveLength(4);
|
||||
expect(spec.layout.spec.items[0].spec).toEqual({
|
||||
expect(spec.layout.kind).toBe('GridLayout');
|
||||
const layout = spec.layout as GridLayoutKind;
|
||||
expect(layout.spec.items).toHaveLength(4);
|
||||
expect(layout.spec.items[0].spec).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '1',
|
||||
@ -533,7 +536,7 @@ describe('ResponseTransformers', () => {
|
||||
},
|
||||
});
|
||||
// Library Panel
|
||||
expect(spec.layout.spec.items[1].spec).toEqual({
|
||||
expect(layout.spec.items[1].spec).toEqual({
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: '2',
|
||||
@ -555,7 +558,7 @@ describe('ResponseTransformers', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const rowSpec = spec.layout.spec.items[2].spec as GridLayoutRowSpec;
|
||||
const rowSpec = layout.spec.items[2].spec as GridLayoutRowSpec;
|
||||
|
||||
expect(rowSpec.collapsed).toBe(false);
|
||||
expect(rowSpec.title).toBe('Row test title');
|
||||
@ -574,7 +577,7 @@ describe('ResponseTransformers', () => {
|
||||
height: 8,
|
||||
});
|
||||
|
||||
const collapsedRowSpec = spec.layout.spec.items[3].spec as GridLayoutRowSpec;
|
||||
const collapsedRowSpec = layout.spec.items[3].spec as GridLayoutRowSpec;
|
||||
expect(collapsedRowSpec.collapsed).toBe(true);
|
||||
expect(collapsedRowSpec.title).toBe('Collapsed row title');
|
||||
expect(collapsedRowSpec.repeat).toBeUndefined();
|
||||
@ -748,9 +751,11 @@ describe('ResponseTransformers', () => {
|
||||
validateAnnotation(dashboard.annotations!.list![3], dashboardV2.spec.annotations[3]);
|
||||
// panel
|
||||
const panelKey = 'panel-1';
|
||||
expect(dashboardV2.spec.elements[panelKey].kind).toBe('Panel');
|
||||
const panelV2 = dashboardV2.spec.elements[panelKey] as PanelKind;
|
||||
expect(panelV2.kind).toBe('Panel');
|
||||
validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout, panelKey);
|
||||
expect(dashboardV2.spec.layout.kind).toBe('GridLayout');
|
||||
validatePanel(dashboard.panels![0], panelV2, dashboardV2.spec.layout as GridLayoutKind, panelKey);
|
||||
// library panel
|
||||
expect(dashboard.panels![1].libraryPanel).toEqual({
|
||||
uid: 'uid-for-library-panel',
|
||||
@ -845,7 +850,7 @@ describe('ResponseTransformers', () => {
|
||||
expect(v1.filter).toEqual(v2Spec.filter);
|
||||
}
|
||||
|
||||
function validatePanel(v1: Panel, v2: PanelKind, layoutV2: DashboardV2Spec['layout'], panelKey: string) {
|
||||
function validatePanel(v1: Panel, v2: PanelKind, layoutV2: GridLayoutKind, panelKey: string) {
|
||||
const { spec: v2Spec } = v2;
|
||||
|
||||
expect(v1.id).toBe(v2Spec.id);
|
||||
|
@ -860,6 +860,10 @@ function getPanelsV1(
|
||||
|
||||
let maxPanelId = 0;
|
||||
|
||||
if (layout.kind !== 'GridLayout') {
|
||||
throw new Error('Cannot convert non-GridLayout layout to v1');
|
||||
}
|
||||
|
||||
for (const item of layout.spec.items) {
|
||||
if (item.kind === 'GridLayoutItem') {
|
||||
const panel = panels[item.spec.element.name];
|
||||
|
Loading…
Reference in New Issue
Block a user