mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: refactor transform scene layout to save model and transform save model to scene layout, schema v2 (#100322)
* Add tests * refactor transformSaveModelToSchemaV2 and transformSceneToSaveModelV2 * move default grid serializer functions outside of class * simplify layoutmanager descriptor * add test for SaveModel -> Scene * Fix lint issues * remove auto added import * Fix name * Fix test typo
This commit is contained in:
parent
e17fd5e8ad
commit
6ee3c71ffe
@ -28,6 +28,7 @@ import {
|
||||
} from '../../utils/utils';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
|
||||
@ -45,7 +46,7 @@ export class DefaultGridLayoutManager
|
||||
|
||||
public readonly isDashboardLayoutManager = true;
|
||||
|
||||
public static readonly descriptor = {
|
||||
public static readonly descriptor: LayoutRegistryItem = {
|
||||
get name() {
|
||||
return t('dashboard.default-layout.name', 'Default grid');
|
||||
},
|
||||
@ -54,6 +55,7 @@ export class DefaultGridLayoutManager
|
||||
},
|
||||
id: 'default-grid',
|
||||
createFromLayout: DefaultGridLayoutManager.createFromLayout,
|
||||
kind: 'GridLayout',
|
||||
};
|
||||
|
||||
public readonly descriptor = DefaultGridLayoutManager.descriptor;
|
||||
|
@ -7,6 +7,7 @@ import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelI
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { ResponsiveGridItem } from './ResponsiveGridItem';
|
||||
import { getEditOptions } from './ResponsiveGridLayoutManagerEditor';
|
||||
@ -23,7 +24,7 @@ export class ResponsiveGridLayoutManager
|
||||
|
||||
public readonly isDashboardLayoutManager = true;
|
||||
|
||||
public static readonly descriptor = {
|
||||
public static readonly descriptor: LayoutRegistryItem = {
|
||||
get name() {
|
||||
return t('dashboard.responsive-layout.name', 'Responsive grid');
|
||||
},
|
||||
@ -32,6 +33,8 @@ export class ResponsiveGridLayoutManager
|
||||
},
|
||||
id: 'responsive-grid',
|
||||
createFromLayout: ResponsiveGridLayoutManager.createFromLayout,
|
||||
|
||||
kind: 'ResponsiveGridLayout',
|
||||
};
|
||||
|
||||
public readonly descriptor = ResponsiveGridLayoutManager.descriptor;
|
||||
|
@ -9,6 +9,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan
|
||||
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
|
||||
@ -23,7 +24,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
|
||||
public readonly isDashboardLayoutManager = true;
|
||||
|
||||
public static readonly descriptor = {
|
||||
public static readonly descriptor: LayoutRegistryItem = {
|
||||
get name() {
|
||||
return t('dashboard.rows-layout.name', 'Rows');
|
||||
},
|
||||
@ -32,6 +33,8 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
},
|
||||
id: 'rows-layout',
|
||||
createFromLayout: RowsLayoutManager.createFromLayout,
|
||||
|
||||
kind: 'RowsLayout',
|
||||
};
|
||||
|
||||
public readonly descriptor = RowsLayoutManager.descriptor;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { LayoutRegistryItem } from './LayoutRegistryItem';
|
||||
@ -82,6 +83,15 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
|
||||
cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
|
||||
}
|
||||
|
||||
export interface LayoutManagerSerializer {
|
||||
serialize(layout: DashboardLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'];
|
||||
deserialize(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
): DashboardLayoutManager;
|
||||
}
|
||||
|
||||
export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager {
|
||||
return 'isDashboardLayoutManager' in obj;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { RegistryItem } from '@grafana/data';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
|
||||
import { DashboardLayoutManager } from './DashboardLayoutManager';
|
||||
|
||||
@ -17,4 +18,9 @@ export interface LayoutRegistryItem<S = {}> extends RegistryItem {
|
||||
* @param saveModel
|
||||
*/
|
||||
createFromSaveModel?(saveModel: S): void;
|
||||
|
||||
/**
|
||||
* Schema kind of layout
|
||||
*/
|
||||
kind?: DashboardV2Spec['layout']['kind'];
|
||||
}
|
||||
|
@ -0,0 +1,354 @@
|
||||
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 { isClonedKey } from '../../utils/clone';
|
||||
import { calculateGridItemDimensions, 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<GridLayoutItemKind | GridLayoutRowKind> {
|
||||
let items: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
|
||||
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 = gridItem_.state.body.state.key ?? 'DefaultName';
|
||||
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<string, Element>): 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[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);
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { SceneCSSGridLayout } from '@grafana/scenes';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
|
||||
import { ResponsiveGridItem } from '../../scene/layout-responsive-grid/ResponsiveGridItem';
|
||||
import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
|
||||
import { getGridItemKeyForPanelId } from '../../utils/utils';
|
||||
|
||||
import { buildVizPanel } from './utils';
|
||||
|
||||
export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
|
||||
serialize(layoutManager: ResponsiveGridLayoutManager): DashboardV2Spec['layout'] {
|
||||
return {
|
||||
kind: 'ResponsiveGridLayout',
|
||||
spec: {
|
||||
col:
|
||||
layoutManager.state.layout.state.templateColumns?.toString() ??
|
||||
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
|
||||
row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows,
|
||||
items: layoutManager.state.layout.state.children.map((child) => {
|
||||
if (!(child instanceof ResponsiveGridItem)) {
|
||||
throw new Error('Expected ResponsiveGridItem');
|
||||
}
|
||||
return {
|
||||
kind: 'ResponsiveGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: child.state?.body?.state.key ?? 'DefaultName',
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(layout: DashboardV2Spec['layout'], elements: DashboardV2Spec['elements']): DashboardLayoutManager {
|
||||
if (layout.kind !== 'ResponsiveGridLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
}
|
||||
|
||||
const children = layout.spec.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),
|
||||
});
|
||||
});
|
||||
|
||||
return new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
templateColumns: layout.spec.col,
|
||||
autoRows: layout.spec.row,
|
||||
children,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
|
||||
import { RowItem } from '../../scene/layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
|
||||
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
|
||||
|
||||
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
|
||||
import { getLayout } from './utils';
|
||||
|
||||
export class RowsLayoutSerializer implements LayoutManagerSerializer {
|
||||
serialize(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] {
|
||||
return {
|
||||
kind: 'RowsLayout',
|
||||
spec: {
|
||||
rows: layoutManager.state.rows.map((row) => {
|
||||
const layout = getLayout(row.state.layout);
|
||||
if (layout.kind === 'RowsLayout') {
|
||||
throw new Error('Nested RowsLayout is not supported');
|
||||
}
|
||||
return {
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: row.state.title,
|
||||
collapsed: row.state.isCollapsed ?? false,
|
||||
layout: layout,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
): RowsLayoutManager {
|
||||
if (layout.kind !== 'RowsLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
}
|
||||
const rows = layout.spec.rows.map((row) => {
|
||||
const layout = row.spec.layout;
|
||||
return new RowItem({
|
||||
title: row.spec.title,
|
||||
isCollapsed: row.spec.collapsed,
|
||||
layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload),
|
||||
});
|
||||
});
|
||||
return new RowsLayoutManager({ rows });
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Registry, RegistryItem } from '@grafana/data';
|
||||
|
||||
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
|
||||
|
||||
import { DefaultGridLayoutManagerSerializer } from './DefaultGridLayoutSerializer';
|
||||
import { ResponsiveGridLayoutSerializer } from './ResponsiveGridLayoutSerializer';
|
||||
import { RowsLayoutSerializer } from './RowsLayoutSerializer';
|
||||
|
||||
interface LayoutSerializerRegistryItem extends RegistryItem {
|
||||
serializer: LayoutManagerSerializer;
|
||||
}
|
||||
|
||||
export const layoutSerializerRegistry: Registry<LayoutSerializerRegistryItem> =
|
||||
new Registry<LayoutSerializerRegistryItem>(() => {
|
||||
return [
|
||||
{ id: 'GridLayout', name: 'Grid Layout', serializer: new DefaultGridLayoutManagerSerializer() },
|
||||
{ id: 'ResponsiveGridLayout', name: 'Responsive Grid Layout', serializer: new ResponsiveGridLayoutSerializer() },
|
||||
{ id: 'RowsLayout', name: 'Rows Layout', serializer: new RowsLayoutSerializer() },
|
||||
];
|
||||
});
|
@ -0,0 +1,154 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
SceneDataProvider,
|
||||
SceneDataQuery,
|
||||
SceneDataTransformer,
|
||||
SceneObject,
|
||||
SceneQueryRunner,
|
||||
VizPanel,
|
||||
VizPanelMenu,
|
||||
VizPanelState,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { DashboardV2Spec, PanelKind, PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../../scene/PanelNotices';
|
||||
import { PanelTimeRange } from '../../scene/PanelTimeRange';
|
||||
import { AngularDeprecation } from '../../scene/angular/AngularDeprecation';
|
||||
import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext';
|
||||
import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager';
|
||||
import { getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { transformMappingsToV1 } from '../transformToV1TypesUtils';
|
||||
|
||||
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
|
||||
|
||||
export function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
const titleItems: SceneObject[] = [];
|
||||
|
||||
if (config.featureToggles.angularDeprecationUI) {
|
||||
titleItems.push(new AngularDeprecation());
|
||||
}
|
||||
|
||||
titleItems.push(
|
||||
new VizPanelLinks({
|
||||
rawLinks: panel.spec.links,
|
||||
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
|
||||
})
|
||||
);
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
|
||||
const queryOptions = panel.spec.data.spec.queryOptions;
|
||||
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: getVizPanelKeyForPanelId(panel.spec.id),
|
||||
title: panel.spec.title,
|
||||
description: panel.spec.description,
|
||||
pluginId: panel.spec.vizConfig.kind,
|
||||
options: panel.spec.vizConfig.spec.options,
|
||||
fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig),
|
||||
pluginVersion: panel.spec.vizConfig.spec.pluginVersion,
|
||||
displayMode: panel.spec.transparent ? 'transparent' : 'default',
|
||||
hoverHeader: !panel.spec.title && !timeOverrideShown,
|
||||
hoverHeaderOffset: 0,
|
||||
$data: createPanelDataProvider(panel),
|
||||
titleItems,
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
|
||||
};
|
||||
|
||||
if (!config.publicDashboardAccessToken) {
|
||||
vizPanelState.menu = new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
});
|
||||
}
|
||||
|
||||
if (queryOptions.timeFrom || queryOptions.timeShift) {
|
||||
vizPanelState.$timeRange = new PanelTimeRange({
|
||||
timeFrom: queryOptions.timeFrom,
|
||||
timeShift: queryOptions.timeShift,
|
||||
hideTimeOverride: queryOptions.hideTimeOverride,
|
||||
});
|
||||
}
|
||||
|
||||
return new VizPanel(vizPanelState);
|
||||
}
|
||||
|
||||
export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider | undefined {
|
||||
const panel = panelKind.spec;
|
||||
const targets = panel.data?.spec.queries ?? [];
|
||||
// Skip setting query runner for panels without queries
|
||||
if (!targets?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Skip setting query runner for panel plugins with skipDataQuery
|
||||
if (config.panels[panel.vizConfig.kind]?.skipDataQuery) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let dataProvider: SceneDataProvider | undefined = undefined;
|
||||
const datasource = getPanelDataSource(panelKind);
|
||||
|
||||
dataProvider = new SceneQueryRunner({
|
||||
datasource,
|
||||
queries: targets.map(panelQueryKindToSceneQuery),
|
||||
maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined,
|
||||
maxDataPointsFromWidth: true,
|
||||
cacheTimeout: panel.data.spec.queryOptions.cacheTimeout,
|
||||
queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL,
|
||||
minInterval: panel.data.spec.queryOptions.interval ?? undefined,
|
||||
dataLayerFilter: {
|
||||
panelId: panel.id,
|
||||
},
|
||||
$behaviors: [new DashboardDatasourceBehaviour({})],
|
||||
});
|
||||
|
||||
// Wrap inner data provider in a data transformer
|
||||
return new SceneDataTransformer({
|
||||
$data: dataProvider,
|
||||
transformations: panel.data.spec.transformations.map((transformation) => transformation.spec),
|
||||
});
|
||||
}
|
||||
|
||||
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
|
||||
if (!panel.spec.data?.spec.queries?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let datasource: DataSourceRef | undefined = undefined;
|
||||
let isMixedDatasource = false;
|
||||
|
||||
panel.spec.data.spec.queries.forEach((query) => {
|
||||
if (!datasource) {
|
||||
datasource = query.spec.datasource;
|
||||
} else if (datasource.uid !== query.spec.datasource?.uid || datasource.type !== query.spec.datasource?.type) {
|
||||
isMixedDatasource = true;
|
||||
}
|
||||
});
|
||||
|
||||
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined;
|
||||
}
|
||||
|
||||
function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery {
|
||||
return {
|
||||
refId: query.spec.refId,
|
||||
datasource: query.spec.datasource,
|
||||
hide: query.spec.hidden,
|
||||
...query.spec.query.spec,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['layout'] {
|
||||
const registryItem = layoutSerializerRegistry.get(sceneState.descriptor.kind ?? '');
|
||||
if (!registryItem) {
|
||||
throw new Error(`Layout serializer not found for kind: ${sceneState.descriptor.kind}`);
|
||||
}
|
||||
return registryItem.serializer.serialize(sceneState);
|
||||
}
|
@ -14,6 +14,7 @@ import {
|
||||
AdHocFiltersVariable,
|
||||
SceneDataTransformer,
|
||||
SceneGridRow,
|
||||
SceneGridItem,
|
||||
} from '@grafana/scenes';
|
||||
import {
|
||||
AdhocVariableKind,
|
||||
@ -34,6 +35,9 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
||||
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
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 { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getQueryRunnerFor } from '../utils/utils';
|
||||
@ -495,5 +499,112 @@ describe('transformSaveModelSchemaV2ToScene', () => {
|
||||
expect(scene.state.meta.canDelete).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('dynamic dashboard layouts', () => {
|
||||
it('should build a dashboard scene with a responsive grid layout', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
dashboard.spec.layout = {
|
||||
kind: 'ResponsiveGridLayout',
|
||||
spec: {
|
||||
col: 'colString',
|
||||
row: 'rowString',
|
||||
items: [
|
||||
{
|
||||
kind: 'ResponsiveGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const scene = transformSaveModelSchemaV2ToScene(dashboard);
|
||||
const layoutManager = scene.state.body as ResponsiveGridLayoutManager;
|
||||
expect(layoutManager.descriptor.kind).toBe('ResponsiveGridLayout');
|
||||
expect(layoutManager.state.layout.state.templateColumns).toBe('colString');
|
||||
expect(layoutManager.state.layout.state.autoRows).toBe('rowString');
|
||||
expect(layoutManager.state.layout.state.children.length).toBe(1);
|
||||
const gridItem = layoutManager.state.layout.state.children[0] as ResponsiveGridItem;
|
||||
expect(gridItem.state.body.state.key).toBe('panel-1');
|
||||
});
|
||||
|
||||
it('should build a dashboard scene with rows layout', () => {
|
||||
const dashboard = cloneDeep(defaultDashboard);
|
||||
dashboard.spec.layout = {
|
||||
kind: 'RowsLayout',
|
||||
spec: {
|
||||
rows: [
|
||||
{
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: 'row1',
|
||||
collapsed: false,
|
||||
layout: {
|
||||
kind: 'ResponsiveGridLayout',
|
||||
spec: {
|
||||
col: 'colString',
|
||||
row: 'rowString',
|
||||
items: [
|
||||
{
|
||||
kind: 'ResponsiveGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: 'row2',
|
||||
collapsed: true,
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
y: 0,
|
||||
x: 0,
|
||||
height: 10,
|
||||
width: 10,
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: 'panel-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const scene = transformSaveModelSchemaV2ToScene(dashboard);
|
||||
const layoutManager = scene.state.body as RowsLayoutManager;
|
||||
expect(layoutManager.descriptor.kind).toBe('RowsLayout');
|
||||
expect(layoutManager.state.rows.length).toBe(2);
|
||||
const row1Manager = layoutManager.state.rows[0].state.layout as ResponsiveGridLayoutManager;
|
||||
expect(row1Manager.descriptor.kind).toBe('ResponsiveGridLayout');
|
||||
const row1GridItem = row1Manager.state.layout.state.children[0] as ResponsiveGridItem;
|
||||
expect(row1GridItem.state.body.state.key).toBe('panel-1');
|
||||
|
||||
const row2Manager = layoutManager.state.rows[1].state.layout as DefaultGridLayoutManager;
|
||||
expect(row2Manager.descriptor.kind).toBe('GridLayout');
|
||||
const row2GridItem = row2Manager.state.grid.state.children[0] as SceneGridItem;
|
||||
expect(row2GridItem.state.body!.state.key).toBe('panel-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,15 +10,10 @@ import {
|
||||
GroupByVariable,
|
||||
IntervalVariable,
|
||||
QueryVariable,
|
||||
SceneCSSGridLayout,
|
||||
SceneDataLayerControls,
|
||||
SceneDataProvider,
|
||||
SceneDataQuery,
|
||||
SceneDataTransformer,
|
||||
SceneGridItemLike,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneObject,
|
||||
SceneQueryRunner,
|
||||
SceneRefreshPicker,
|
||||
SceneTimePicker,
|
||||
@ -27,9 +22,6 @@ import {
|
||||
SceneVariableSet,
|
||||
TextBoxVariable,
|
||||
VariableValueSelectors,
|
||||
VizPanel,
|
||||
VizPanelMenu,
|
||||
VizPanelState,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
|
||||
import {
|
||||
@ -46,9 +38,6 @@ import {
|
||||
defaultIntervalVariableKind,
|
||||
defaultQueryVariableKind,
|
||||
defaultTextVariableKind,
|
||||
GridLayoutItemSpec,
|
||||
GridLayoutKind,
|
||||
Element,
|
||||
GroupByVariableKind,
|
||||
IntervalVariableKind,
|
||||
LibraryPanelKind,
|
||||
@ -56,9 +45,7 @@ import {
|
||||
PanelQueryKind,
|
||||
QueryVariableKind,
|
||||
TextVariableKind,
|
||||
ResponsiveGridLayoutItemKind,
|
||||
} from '@grafana/schema/src/schema/dashboard/v2alpha0';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import {
|
||||
AnnoKeyCreatedBy,
|
||||
AnnoKeyFolder,
|
||||
@ -80,32 +67,16 @@ import { registerDashboardMacro } from '../scene/DashboardMacro';
|
||||
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardScopesFacade } from '../scene/DashboardScopesFacade';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../scene/PanelNotices';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
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 { 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 { getGridItemKeyForPanelId, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
import { getIntervalsFromQueryString } from '../utils/utils';
|
||||
|
||||
import { GRID_ROW_HEIGHT } from './const';
|
||||
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
|
||||
import { layoutSerializerRegistry } from './layoutSerializers/layoutSerializerRegistry';
|
||||
import { registerPanelInteractionsReporter } from './transformSaveModelToScene';
|
||||
import {
|
||||
transformCursorSyncV2ToV1,
|
||||
transformSortVariableToEnumV1,
|
||||
transformMappingsToV1,
|
||||
transformVariableHideToEnumV1,
|
||||
transformVariableRefreshToEnumV1,
|
||||
} from './transformToV1TypesUtils';
|
||||
@ -179,7 +150,11 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
meta.canSave = false;
|
||||
}
|
||||
|
||||
const layoutManager: DashboardLayoutManager = createLayoutManager(dashboard);
|
||||
const layoutManager: DashboardLayoutManager = layoutSerializerRegistry
|
||||
.get(dashboard.layout.kind)
|
||||
.serializer.deserialize(dashboard.layout, dashboard.elements, dashboard.preload);
|
||||
|
||||
//createLayoutManager(dashboard);
|
||||
|
||||
const dashboardScene = new DashboardScene({
|
||||
description: dashboard.description,
|
||||
@ -243,251 +218,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
return dashboardScene;
|
||||
}
|
||||
|
||||
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 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 = 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[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 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);
|
||||
}
|
||||
|
||||
function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
const titleItems: SceneObject[] = [];
|
||||
|
||||
if (config.featureToggles.angularDeprecationUI) {
|
||||
titleItems.push(new AngularDeprecation());
|
||||
}
|
||||
|
||||
titleItems.push(
|
||||
new VizPanelLinks({
|
||||
rawLinks: panel.spec.links,
|
||||
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
|
||||
})
|
||||
);
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
|
||||
const queryOptions = panel.spec.data.spec.queryOptions;
|
||||
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: getVizPanelKeyForPanelId(panel.spec.id),
|
||||
title: panel.spec.title,
|
||||
description: panel.spec.description,
|
||||
pluginId: panel.spec.vizConfig.kind,
|
||||
options: panel.spec.vizConfig.spec.options,
|
||||
fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig),
|
||||
pluginVersion: panel.spec.vizConfig.spec.pluginVersion,
|
||||
displayMode: panel.spec.transparent ? 'transparent' : 'default',
|
||||
hoverHeader: !panel.spec.title && !timeOverrideShown,
|
||||
hoverHeaderOffset: 0,
|
||||
$data: createPanelDataProvider(panel),
|
||||
titleItems,
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
|
||||
};
|
||||
|
||||
if (!config.publicDashboardAccessToken) {
|
||||
vizPanelState.menu = new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
});
|
||||
}
|
||||
|
||||
if (queryOptions.timeFrom || queryOptions.timeShift) {
|
||||
vizPanelState.$timeRange = new PanelTimeRange({
|
||||
timeFrom: queryOptions.timeFrom,
|
||||
timeShift: queryOptions.timeShift,
|
||||
hideTimeOverride: queryOptions.hideTimeOverride,
|
||||
});
|
||||
}
|
||||
|
||||
return new VizPanel(vizPanelState);
|
||||
}
|
||||
|
||||
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
|
||||
if (!panel.spec.data?.spec.queries?.length) {
|
||||
return undefined;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
GroupByVariable,
|
||||
IntervalVariable,
|
||||
QueryVariable,
|
||||
SceneCSSGridLayout,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneRefreshPicker,
|
||||
@ -24,6 +25,10 @@ import {
|
||||
VariableSort as VariableSortV1,
|
||||
} from '@grafana/schema/dist/esm/index.gen';
|
||||
|
||||
import {
|
||||
ResponsiveGridLayoutSpec,
|
||||
RowsLayoutSpec,
|
||||
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0';
|
||||
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
@ -33,6 +38,11 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
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 { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||
|
||||
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
|
||||
|
||||
@ -365,6 +375,158 @@ describe('transformSceneToSaveModelSchemaV2', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function getMinimalSceneState(body: DashboardLayoutManager): Partial<DashboardSceneState> {
|
||||
return {
|
||||
id: 1,
|
||||
title: 'Test Dashboard',
|
||||
description: 'Test Description',
|
||||
preload: true,
|
||||
tags: ['tag1', 'tag2'],
|
||||
uid: 'test-uid',
|
||||
version: 1,
|
||||
|
||||
controls: new DashboardControls({
|
||||
refreshPicker: new SceneRefreshPicker({
|
||||
refresh: '5s',
|
||||
intervals: ['5s', '10s', '30s'],
|
||||
autoEnabled: true,
|
||||
autoMinInterval: '5s',
|
||||
autoValue: '5s',
|
||||
isOnCanvas: true,
|
||||
primary: true,
|
||||
withText: true,
|
||||
minRefreshInterval: '5s',
|
||||
}),
|
||||
timePicker: new SceneTimePicker({
|
||||
isOnCanvas: true,
|
||||
hidePicker: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
$timeRange: new SceneTimeRange({
|
||||
timeZone: 'UTC',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
weekStart: 'monday',
|
||||
fiscalYearStartMonth: 1,
|
||||
UNSAFE_nowDelay: '1m',
|
||||
refreshOnActivate: {
|
||||
afterMs: 10,
|
||||
percent: 0.1,
|
||||
},
|
||||
}),
|
||||
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
describe('dynamic layouts', () => {
|
||||
it('should transform scene with rows layout with default grids in rows to save model schema v2', () => {
|
||||
const scene = setupDashboardScene(
|
||||
getMinimalSceneState(
|
||||
new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
layout: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
y: 0,
|
||||
height: 10,
|
||||
body: new VizPanel({}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const result = transformSceneToSaveModelSchemaV2(scene);
|
||||
expect(result.layout.kind).toBe('RowsLayout');
|
||||
const rowsLayout = result.layout.spec as RowsLayoutSpec;
|
||||
expect(rowsLayout.rows.length).toBe(1);
|
||||
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
|
||||
expect(rowsLayout.rows[0].spec.layout.kind).toBe('GridLayout');
|
||||
});
|
||||
|
||||
it('should transform scene with rows layout with multiple rows with different grids to save model schema v2', () => {
|
||||
const scene = setupDashboardScene(
|
||||
getMinimalSceneState(
|
||||
new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
layout: new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
children: [
|
||||
new ResponsiveGridItem({
|
||||
body: new VizPanel({}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
new RowItem({
|
||||
layout: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
y: 0,
|
||||
height: 10,
|
||||
body: new VizPanel({}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const result = transformSceneToSaveModelSchemaV2(scene);
|
||||
expect(result.layout.kind).toBe('RowsLayout');
|
||||
const rowsLayout = result.layout.spec as RowsLayoutSpec;
|
||||
expect(rowsLayout.rows.length).toBe(2);
|
||||
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
|
||||
expect(rowsLayout.rows[0].spec.layout.kind).toBe('ResponsiveGridLayout');
|
||||
expect(rowsLayout.rows[0].spec.layout.spec.items[0].kind).toBe('ResponsiveGridLayoutItem');
|
||||
|
||||
expect(rowsLayout.rows[1].spec.layout.kind).toBe('GridLayout');
|
||||
expect(rowsLayout.rows[1].spec.layout.spec.items[0].kind).toBe('GridLayoutItem');
|
||||
});
|
||||
|
||||
it('should transform scene with responsive grid layout to schema v2', () => {
|
||||
const scene = setupDashboardScene(
|
||||
getMinimalSceneState(
|
||||
new ResponsiveGridLayoutManager({
|
||||
layout: new SceneCSSGridLayout({
|
||||
autoRows: 'rowString',
|
||||
templateColumns: 'colString',
|
||||
children: [
|
||||
new ResponsiveGridItem({
|
||||
body: new VizPanel({}),
|
||||
}),
|
||||
new ResponsiveGridItem({
|
||||
body: new VizPanel({}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = transformSceneToSaveModelSchemaV2(scene);
|
||||
expect(result.layout.kind).toBe('ResponsiveGridLayout');
|
||||
const respGridLayout = result.layout.spec as ResponsiveGridLayoutSpec;
|
||||
expect(respGridLayout.col).toBe('colString');
|
||||
expect(respGridLayout.row).toBe('rowString');
|
||||
expect(respGridLayout.items.length).toBe(2);
|
||||
expect(respGridLayout.items[0].kind).toBe('ResponsiveGridLayoutItem');
|
||||
});
|
||||
});
|
||||
|
||||
const annotationLayer1 = new DashboardAnnotationsDataLayer({
|
||||
key: 'layer1',
|
||||
query: {
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
dataLayers,
|
||||
SceneDataQuery,
|
||||
SceneDataTransformer,
|
||||
SceneGridRow,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
@ -24,7 +23,6 @@ import {
|
||||
DataTransformerConfig,
|
||||
PanelQuerySpec,
|
||||
DataQueryKind,
|
||||
GridLayoutItemKind,
|
||||
QueryOptionsSpec,
|
||||
QueryVariableKind,
|
||||
TextVariableKind,
|
||||
@ -38,27 +36,13 @@ import {
|
||||
DataLink,
|
||||
LibraryPanelKind,
|
||||
Element,
|
||||
RepeatOptions,
|
||||
GridLayoutRowKind,
|
||||
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';
|
||||
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 {
|
||||
getLibraryPanelBehavior,
|
||||
@ -66,10 +50,9 @@ import {
|
||||
getQueryRunnerFor,
|
||||
getVizPanelKeyForPanelId,
|
||||
isLibraryPanel,
|
||||
calculateGridItemDimensions,
|
||||
} from '../utils/utils';
|
||||
|
||||
import { GRID_ROW_HEIGHT } from './const';
|
||||
import { getLayout } from './layoutSerializers/utils';
|
||||
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
|
||||
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';
|
||||
|
||||
@ -127,7 +110,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
|
||||
// EOF annotations
|
||||
|
||||
// layout
|
||||
layout: getLayout(sceneDash.body, isSnapshot),
|
||||
layout: getLayout(sceneDash.body),
|
||||
// EOF layout
|
||||
};
|
||||
|
||||
@ -144,75 +127,6 @@ 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;
|
||||
@ -231,146 +145,6 @@ function getLiveNow(state: DashboardSceneState) {
|
||||
return Boolean(liveNow);
|
||||
}
|
||||
|
||||
function getGridLayoutItems(
|
||||
body: DefaultGridLayoutManager,
|
||||
isSnapshot?: boolean
|
||||
): Array<GridLayoutItemKind | GridLayoutRowKind> {
|
||||
let elements: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
|
||||
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,
|
||||
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 = gridItem_.state.body.state.key ?? 'DefaultName';
|
||||
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 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, isSnapshot, 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 getElements(state: DashboardSceneState) {
|
||||
const panels = state.body.getVizPanels() ?? [];
|
||||
|
||||
@ -577,60 +351,6 @@ function createElements(panels: Element[]): Record<string, Element> {
|
||||
}, {});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user