mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 23:42:51 +08:00
Dynamic dashboards: Copy paste rows tabs and auto grid items (#103237)
* copy paste tab row works * refactor to hook * add buttons to canvas * make i18n * remove paste from add pane and refactor * pasting auto grid panel works * add paste for default grid * set height/width and postion for auto grid items moving to default grid * clean up
This commit is contained in:
@ -9,7 +9,8 @@ export const DEFAULT_ROW_HEIGHT = 250;
|
||||
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
||||
|
||||
export const LS_PANEL_COPY_KEY = 'panel-copy';
|
||||
|
||||
export const LS_ROW_COPY_KEY = 'row-copy';
|
||||
export const LS_TAB_COPY_KEY = 'tab-copy';
|
||||
export const PANEL_BORDER = 2;
|
||||
|
||||
export const EDIT_PANEL_ID = 23763571993;
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Box, Card, Icon, IconButton, IconName, useStyles2 } from '@grafana/ui';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
@ -29,15 +26,6 @@ interface CardConfig {
|
||||
export function DashboardAddPane({ editPane }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dashboard = getDashboardSceneFor(editPane);
|
||||
const [hasCopiedPanel, setHasCopiedPanel] = useState(store.exists(LS_PANEL_COPY_KEY));
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(LS_PANEL_COPY_KEY, () => {
|
||||
setHasCopiedPanel(store.exists(LS_PANEL_COPY_KEY));
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const cards: CardConfig[] = [
|
||||
{
|
||||
@ -74,14 +62,6 @@ export function DashboardAddPane({ editPane }: Props) {
|
||||
testId: selectors.components.PageToolbar.itemButton('add_tab'),
|
||||
onClick: () => dashboard.onCreateNewTab(),
|
||||
},
|
||||
{
|
||||
hide: !hasCopiedPanel,
|
||||
icon: 'clipboard-alt',
|
||||
heading: t('dashboard.edit-pane.add.paste-panel.heading', 'Paste panel'),
|
||||
title: t('dashboard.edit-pane.add.paste-panel.title', 'Paste a panel from the clipboard'),
|
||||
testId: selectors.components.PageToolbar.itemButton('paste_panel'),
|
||||
onClick: () => dashboard.pastePanel(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as H from 'history';
|
||||
|
||||
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
||||
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
||||
import { config, locationService, RefreshEvent } from '@grafana/runtime';
|
||||
import {
|
||||
sceneGraph,
|
||||
@ -40,6 +40,9 @@ import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTrack
|
||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||
import { DashboardChangeInfo } from '../saving/shared';
|
||||
import { DashboardSceneSerializerLike, getDashboardSceneSerializer } from '../serialization/DashboardSceneSerializer';
|
||||
import { gridItemToGridLayoutItemKind } from '../serialization/layoutSerializers/DefaultGridLayoutSerializer';
|
||||
import { serializeAutoGridItem } from '../serialization/layoutSerializers/ResponsiveGridLayoutSerializer';
|
||||
import { getElement } from '../serialization/layoutSerializers/utils';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
@ -71,8 +74,10 @@ import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './ang
|
||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { AutoGridItem } from './layout-responsive-grid/ResponsiveGridItem';
|
||||
import { LayoutRestorer } from './layouts-shared/LayoutRestorer';
|
||||
import { addNewRowTo, addNewTabTo } from './layouts-shared/addNew';
|
||||
import { clearClipboard } from './layouts-shared/paste';
|
||||
import { DashboardLayoutManager } from './types/DashboardLayoutManager';
|
||||
import { isLayoutParent, LayoutParent } from './types/LayoutParent';
|
||||
|
||||
@ -523,6 +528,28 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
}
|
||||
|
||||
public copyPanel(vizPanel: VizPanel) {
|
||||
if (config.featureToggles.dashboardNewLayouts) {
|
||||
const gridItem = vizPanel.parent;
|
||||
|
||||
if (gridItem instanceof AutoGridItem) {
|
||||
const elements = getElement(gridItem, this);
|
||||
const gridItemKind = serializeAutoGridItem(gridItem);
|
||||
|
||||
clearClipboard();
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ elements, gridItem: gridItemKind }));
|
||||
} else if (gridItem instanceof DashboardGridItem) {
|
||||
const elements = getElement(gridItem, this);
|
||||
const gridItemKind = gridItemToGridLayoutItemKind(gridItem);
|
||||
|
||||
clearClipboard();
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify({ elements, gridItem: gridItemKind }));
|
||||
} else {
|
||||
console.error('Trying to copy a panel that is not DashboardGridItem child');
|
||||
throw new Error('Trying to copy a panel that is not DashboardGridItem child');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vizPanel.parent) {
|
||||
return;
|
||||
}
|
||||
@ -536,8 +563,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
|
||||
const jsonData = gridItemToPanel(gridItem);
|
||||
|
||||
clearClipboard();
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify(jsonData));
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Use **Paste panel** toolbar action to paste.']);
|
||||
}
|
||||
|
||||
public pastePanel() {
|
||||
|
@ -38,8 +38,10 @@ import {
|
||||
getGridItemKeyForPanelId,
|
||||
useDashboard,
|
||||
getLayoutOrchestratorFor,
|
||||
getDashboardSceneFor,
|
||||
} from '../../utils/utils';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-shared/paste';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@ -134,6 +136,14 @@ export class DefaultGridLayoutManager
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true);
|
||||
}
|
||||
|
||||
public pastePanel() {
|
||||
const emptySpace = findSpaceForNewPanel(this.state.grid);
|
||||
const panel = getDashboardGridItemFromClipboard(getDashboardSceneFor(this), emptySpace);
|
||||
this.state.grid.setState({ children: [...this.state.grid.state.children, panel] });
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(panel), true);
|
||||
clearClipboard();
|
||||
}
|
||||
|
||||
public removePanel(panel: VizPanel) {
|
||||
const gridItem = panel.parent!;
|
||||
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
getPanelIdForVizPanel,
|
||||
getVizPanelKeyForPanelId,
|
||||
} from '../../utils/utils';
|
||||
import { clearClipboard, getAutoGridItemFromClipboard } from '../layouts-shared/paste';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@ -98,6 +99,13 @@ export class AutoGridLayoutManager
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true);
|
||||
}
|
||||
|
||||
public pastePanel() {
|
||||
const panel = getAutoGridItemFromClipboard(getDashboardSceneFor(this));
|
||||
this.state.layout.setState({ children: [...this.state.layout.state.children, panel] });
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(panel), true);
|
||||
clearClipboard();
|
||||
}
|
||||
|
||||
public removePanel(panel: VizPanel) {
|
||||
const element = panel.parent;
|
||||
this.state.layout.setState({ children: this.state.layout.state.children.filter((child) => child !== element) });
|
||||
|
@ -8,14 +8,20 @@ import {
|
||||
VariableDependencyConfig,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { LS_ROW_COPY_KEY } from 'app/core/constants';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { serializeRow } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
|
||||
import { getElements } from '../../serialization/layoutSerializers/utils';
|
||||
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
|
||||
import { AutoGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { LayoutRestorer } from '../layouts-shared/LayoutRestorer';
|
||||
import { clearClipboard } from '../layouts-shared/paste';
|
||||
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardDropTarget } from '../types/DashboardDropTarget';
|
||||
@ -116,6 +122,17 @@ export class RowItem
|
||||
return this.clone({ key: undefined, layout: this.getLayout().duplicate() });
|
||||
}
|
||||
|
||||
public serialize(): RowsLayoutRowKind {
|
||||
return serializeRow(this);
|
||||
}
|
||||
|
||||
public onCopy() {
|
||||
const elements = getElements(this.getLayout(), getDashboardSceneFor(this));
|
||||
|
||||
clearClipboard();
|
||||
store.set(LS_ROW_COPY_KEY, JSON.stringify({ elements, row: this.serialize() }));
|
||||
}
|
||||
|
||||
public onAddPanel(panel = getDefaultVizPanel()) {
|
||||
this.getLayout().addPanel(panel);
|
||||
}
|
||||
|
@ -10,10 +10,12 @@ import {
|
||||
import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { getRowFromClipboard } from '../layouts-shared/paste';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@ -91,17 +93,23 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true);
|
||||
}
|
||||
|
||||
public addNewRow(): RowItem {
|
||||
const row = new RowItem({ isNew: true });
|
||||
this.setState({ rows: [...this.state.rows, row] });
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(row), true);
|
||||
return row;
|
||||
public addNewRow(row?: RowItem): RowItem {
|
||||
const newRow = row ?? new RowItem({ isNew: true });
|
||||
this.setState({ rows: [...this.state.rows, newRow] });
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true);
|
||||
return newRow;
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
this.state.rows.forEach((row) => row.getLayout().editModeChanged?.(isEditing));
|
||||
}
|
||||
|
||||
public pasteRow() {
|
||||
const scene = getDashboardSceneFor(this);
|
||||
const row = getRowFromClipboard(scene);
|
||||
this.addNewRow(row);
|
||||
}
|
||||
|
||||
public activateRepeaters() {
|
||||
this.state.rows.forEach((row) => {
|
||||
if (!row.isActive) {
|
||||
|
@ -6,6 +6,7 @@ import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
|
||||
import { RowsLayoutManager } from './RowsLayoutManager';
|
||||
|
||||
@ -13,6 +14,7 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
|
||||
const { rows } = model.useState();
|
||||
const { isEditing } = useDashboardState(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
const { hasCopiedRow } = useClipboardState();
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
@ -24,6 +26,11 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
|
||||
<Button icon="plus" variant="primary" fill="text" onClick={() => model.addNewRow()}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.new-row">New row</Trans>
|
||||
</Button>
|
||||
{hasCopiedRow && (
|
||||
<Button icon="plus" variant="primary" fill="text" onClick={() => model.pasteRow()}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.paste-row">Paste row</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -8,13 +8,19 @@ import {
|
||||
SceneObject,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { LS_TAB_COPY_KEY } from 'app/core/constants';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import store from 'app/core/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
|
||||
import { getElements } from '../../serialization/layoutSerializers/utils';
|
||||
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
|
||||
import { AutoGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { LayoutRestorer } from '../layouts-shared/LayoutRestorer';
|
||||
import { clearClipboard } from '../layouts-shared/paste';
|
||||
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardDropTarget } from '../types/DashboardDropTarget';
|
||||
@ -90,6 +96,16 @@ export class TabItem
|
||||
layout.removeTab(this);
|
||||
}
|
||||
|
||||
public serialize(): TabsLayoutTabKind {
|
||||
return serializeTab(this);
|
||||
}
|
||||
|
||||
public onCopy() {
|
||||
const elements = getElements(this.getLayout(), getDashboardSceneFor(this));
|
||||
clearClipboard();
|
||||
store.set(LS_TAB_COPY_KEY, JSON.stringify({ elements, tab: this.serialize() }));
|
||||
}
|
||||
|
||||
public createMultiSelectedElement(items: SceneObject[]): TabItems {
|
||||
return new TabItems(items.filter((item) => item instanceof TabItem));
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ import {
|
||||
ObjectsReorderedOnCanvasEvent,
|
||||
} from '../../edit-pane/shared';
|
||||
import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { RowItem } from '../layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { getTabFromClipboard } from '../layouts-shared/paste';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@ -121,8 +123,8 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const newTab = new TabItem({ isNew: true });
|
||||
public addNewTab(tab?: TabItem) {
|
||||
const newTab = tab ?? new TabItem({ isNew: true });
|
||||
this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length });
|
||||
this.publishEvent(new NewObjectAddedToCanvasEvent(newTab), true);
|
||||
return newTab;
|
||||
@ -132,6 +134,12 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
this.state.tabs.forEach((tab) => tab.getLayout().editModeChanged?.(isEditing));
|
||||
}
|
||||
|
||||
public pasteTab() {
|
||||
const scene = getDashboardSceneFor(this);
|
||||
const tab = getTabFromClipboard(scene);
|
||||
this.addNewTab(tab);
|
||||
}
|
||||
|
||||
public activateRepeaters() {
|
||||
this.state.tabs.forEach((tab) => tab.getLayout().activateRepeaters?.());
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
||||
@ -17,6 +18,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
const { layout } = currentTab.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
const { hasCopiedTab } = useClipboardState();
|
||||
|
||||
return (
|
||||
<div className={styles.tabLayoutContainer}>
|
||||
@ -34,6 +36,11 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
<Button icon="plus" variant="primary" fill="text" onClick={() => model.addNewTab()}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.new-tab">New tab</Trans>
|
||||
</Button>
|
||||
{hasCopiedTab && (
|
||||
<Button icon="plus" variant="primary" fill="text" onClick={() => model.pasteTab()}>
|
||||
<Trans i18nKey="dashboard.canvas-actions.paste-tab">Paste tab</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { addNewRowTo, addNewTabTo } from './addNew';
|
||||
import { useClipboardState } from './useClipboardState';
|
||||
|
||||
export interface Props {
|
||||
layoutManager: DashboardLayoutManager;
|
||||
@ -15,6 +16,7 @@ export interface Props {
|
||||
|
||||
export function CanvasGridAddActions({ layoutManager }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { hasCopiedPanel } = useClipboardState();
|
||||
|
||||
return (
|
||||
<div className={cx(styles.addAction, 'dashboard-canvas-add-button')}>
|
||||
@ -50,6 +52,18 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
|
||||
<Trans i18nKey="dashboard.canvas-actions.group-panels">Group panels</Trans>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{hasCopiedPanel && layoutManager.pastePanel && (
|
||||
<Button
|
||||
variant="primary"
|
||||
fill="text"
|
||||
icon="layers"
|
||||
onClick={() => {
|
||||
layoutManager.pastePanel?.();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.paste-panel">Paste panel</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,116 @@
|
||||
import {
|
||||
AutoGridLayoutItemKind,
|
||||
DashboardV2Spec,
|
||||
GridLayoutItemKind,
|
||||
RowsLayoutRowKind,
|
||||
TabsLayoutTabKind,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { LS_PANEL_COPY_KEY, LS_ROW_COPY_KEY, LS_TAB_COPY_KEY } from 'app/core/constants';
|
||||
import store from 'app/core/store';
|
||||
|
||||
import { deserializeGridItem } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
|
||||
import { deserializeAutoGridItem } from '../../serialization/layoutSerializers/ResponsiveGridLayoutSerializer';
|
||||
import { deserializeRow } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
|
||||
import { deserializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
|
||||
import { GridCell } from '../layout-default/findSpaceForNewPanel';
|
||||
import { AutoGridItem } from '../layout-responsive-grid/ResponsiveGridItem';
|
||||
import { RowItem } from '../layout-rows/RowItem';
|
||||
import { TabItem } from '../layout-tabs/TabItem';
|
||||
|
||||
export function clearClipboard() {
|
||||
store.delete(LS_PANEL_COPY_KEY);
|
||||
store.delete(LS_ROW_COPY_KEY);
|
||||
store.delete(LS_TAB_COPY_KEY);
|
||||
}
|
||||
|
||||
export interface RowStore {
|
||||
elements: DashboardV2Spec['elements'];
|
||||
row: RowsLayoutRowKind;
|
||||
}
|
||||
|
||||
export interface TabStore {
|
||||
elements: DashboardV2Spec['elements'];
|
||||
tab: TabsLayoutTabKind;
|
||||
}
|
||||
|
||||
export interface PanelStore {
|
||||
elements: DashboardV2Spec['elements'];
|
||||
gridItem: GridLayoutItemKind | AutoGridLayoutItemKind;
|
||||
}
|
||||
|
||||
export function getRowFromClipboard(scene: DashboardScene): RowItem {
|
||||
const jsonData = store.get(LS_ROW_COPY_KEY);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const jsonObj: RowStore = JSON.parse(jsonData) as RowStore;
|
||||
clearClipboard();
|
||||
const panelIdGenerator = getPanelIdGenerator(dashboardSceneGraph.getNextPanelId(scene));
|
||||
|
||||
let row;
|
||||
// We don't control the local storage content, so if it's out of sync with the code all bets are off.
|
||||
try {
|
||||
row = deserializeRow(jsonObj.row, jsonObj.elements, false, panelIdGenerator);
|
||||
} catch (error) {
|
||||
throw new Error('Error pasting row from clipboard, please try to copy again');
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
export function getTabFromClipboard(scene: DashboardScene): TabItem {
|
||||
const jsonData = store.get(LS_TAB_COPY_KEY);
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const jsonObj: TabStore = JSON.parse(jsonData) as TabStore;
|
||||
clearClipboard();
|
||||
const panelIdGenerator = getPanelIdGenerator(dashboardSceneGraph.getNextPanelId(scene));
|
||||
let tab;
|
||||
try {
|
||||
tab = deserializeTab(jsonObj.tab, jsonObj.elements, false, panelIdGenerator);
|
||||
} catch (error) {
|
||||
throw new Error('Error pasting tab from clipboard, please try to copy again');
|
||||
}
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function getPanelFromClipboard(scene: DashboardScene): DashboardGridItem | AutoGridItem {
|
||||
const jsonData = store.get(LS_PANEL_COPY_KEY);
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const { elements, gridItem }: PanelStore = JSON.parse(jsonData) as PanelStore;
|
||||
|
||||
if (gridItem.kind === 'GridLayoutItem') {
|
||||
return deserializeGridItem(gridItem, elements, getPanelIdGenerator(dashboardSceneGraph.getNextPanelId(scene)));
|
||||
}
|
||||
return deserializeAutoGridItem(gridItem, elements, getPanelIdGenerator(dashboardSceneGraph.getNextPanelId(scene)));
|
||||
}
|
||||
|
||||
export function getAutoGridItemFromClipboard(scene: DashboardScene): AutoGridItem {
|
||||
const panel = getPanelFromClipboard(scene);
|
||||
if (panel instanceof AutoGridItem) {
|
||||
return panel;
|
||||
}
|
||||
// Convert to AutoGridItem
|
||||
return new AutoGridItem({ body: panel.state.body, key: panel.state.key, variableName: panel.state.variableName });
|
||||
}
|
||||
|
||||
export function getDashboardGridItemFromClipboard(scene: DashboardScene, gridCell: GridCell | null): DashboardGridItem {
|
||||
const panel = getPanelFromClipboard(scene);
|
||||
if (panel instanceof DashboardGridItem) {
|
||||
return panel;
|
||||
}
|
||||
// Convert to DashboardGridItem
|
||||
return new DashboardGridItem({
|
||||
...gridCell,
|
||||
body: panel.state.body,
|
||||
key: panel.state.key,
|
||||
variableName: panel.state.variableName,
|
||||
});
|
||||
}
|
||||
|
||||
function getPanelIdGenerator(start: number) {
|
||||
let id = start;
|
||||
return () => id++;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { LS_PANEL_COPY_KEY, LS_ROW_COPY_KEY, LS_TAB_COPY_KEY } from 'app/core/constants';
|
||||
import store from 'app/core/store';
|
||||
|
||||
export function useClipboardState() {
|
||||
const [hasCopiedPanel, setHasCopiedPanel] = useState(store.exists(LS_PANEL_COPY_KEY));
|
||||
const [hasCopiedRow, setHasCopiedRow] = useState(store.exists(LS_ROW_COPY_KEY));
|
||||
const [hasCopiedTab, setHasCopiedTab] = useState(store.exists(LS_TAB_COPY_KEY));
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(LS_PANEL_COPY_KEY, () => {
|
||||
setHasCopiedPanel(store.exists(LS_PANEL_COPY_KEY));
|
||||
});
|
||||
|
||||
const unsubscribeRow = store.subscribe(LS_ROW_COPY_KEY, () => {
|
||||
setHasCopiedRow(store.exists(LS_ROW_COPY_KEY));
|
||||
});
|
||||
|
||||
const unsubscribeTab = store.subscribe(LS_TAB_COPY_KEY, () => {
|
||||
setHasCopiedTab(store.exists(LS_TAB_COPY_KEY));
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
unsubscribeRow();
|
||||
unsubscribeTab();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
hasCopiedPanel,
|
||||
hasCopiedRow,
|
||||
hasCopiedTab,
|
||||
};
|
||||
}
|
@ -76,6 +76,11 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
|
||||
* Duplicate, like clone but with new keys
|
||||
*/
|
||||
duplicate(): DashboardLayoutManager;
|
||||
|
||||
/**
|
||||
* Paste a panel from the clipboard
|
||||
*/
|
||||
pastePanel?(): void;
|
||||
}
|
||||
|
||||
export interface LayoutManagerSerializer {
|
||||
|
@ -38,7 +38,8 @@ export function serializeDefaultGridLayout(
|
||||
export function deserializeDefaultGridLayout(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): DefaultGridLayoutManager {
|
||||
if (layout.kind !== 'GridLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
@ -46,7 +47,7 @@ export function deserializeDefaultGridLayout(
|
||||
return new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'),
|
||||
children: createSceneGridLayoutForItems(layout, elements),
|
||||
children: createSceneGridLayoutForItems(layout, elements, panelIdGenerator),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@ -108,7 +109,7 @@ function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayo
|
||||
};
|
||||
}
|
||||
|
||||
function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind {
|
||||
export function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind {
|
||||
let elementGridItem: GridLayoutItemKind | undefined;
|
||||
let x = 0,
|
||||
y = 0,
|
||||
@ -223,34 +224,37 @@ function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false):
|
||||
}
|
||||
}
|
||||
|
||||
function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<string, Element>): SceneGridItemLike[] {
|
||||
const gridElements = layout.spec.items;
|
||||
function createSceneGridLayoutForItems(
|
||||
layout: GridLayoutKind,
|
||||
elements: Record<string, Element>,
|
||||
panelIdGenerator?: () => number
|
||||
): SceneGridItemLike[] {
|
||||
const gridItems = 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`);
|
||||
}
|
||||
return buildGridItem(element.spec, panel);
|
||||
} else if (element.kind === 'GridLayoutRow') {
|
||||
const children = element.spec.elements.map((gridElement) => {
|
||||
return gridItems.map((item) => {
|
||||
if (item.kind === 'GridLayoutItem') {
|
||||
return deserializeGridItem(item, elements, panelIdGenerator);
|
||||
} else if (item.kind === 'GridLayoutRow') {
|
||||
const children = item.spec.elements.map((gridElement) => {
|
||||
const panel = elements[getOriginalKey(gridElement.spec.element.name)];
|
||||
if (panel.kind === 'Panel' || panel.kind === 'LibraryPanel') {
|
||||
return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y);
|
||||
let id: number | undefined;
|
||||
if (panelIdGenerator) {
|
||||
id = panelIdGenerator();
|
||||
}
|
||||
return buildGridItem(gridElement.spec, panel, item.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y, id);
|
||||
} 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 })];
|
||||
if (item.spec.repeat) {
|
||||
behaviors = [new RowRepeaterBehavior({ variableName: item.spec.repeat.value })];
|
||||
}
|
||||
return new SceneGridRow({
|
||||
y: element.spec.y,
|
||||
isCollapsed: element.spec.collapsed,
|
||||
title: element.spec.title,
|
||||
y: item.spec.y,
|
||||
isCollapsed: item.spec.collapsed,
|
||||
title: item.spec.title,
|
||||
$behaviors: behaviors,
|
||||
actions: new RowActions({}),
|
||||
children,
|
||||
@ -258,7 +262,7 @@ function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<
|
||||
} 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}`);
|
||||
throw new Error(`Unknown layout element kind: ${item.kind}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -266,16 +270,17 @@ function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<
|
||||
function buildGridItem(
|
||||
gridItem: GridLayoutItemSpec,
|
||||
panel: PanelKind | LibraryPanelKind,
|
||||
yOverride?: number
|
||||
yOverride?: number,
|
||||
id?: number
|
||||
): DashboardGridItem {
|
||||
let vizPanel: VizPanel;
|
||||
if (panel.kind === 'Panel') {
|
||||
vizPanel = buildVizPanel(panel);
|
||||
vizPanel = buildVizPanel(panel, id);
|
||||
} else {
|
||||
vizPanel = buildLibraryPanel(panel);
|
||||
vizPanel = buildLibraryPanel(panel, id);
|
||||
}
|
||||
return new DashboardGridItem({
|
||||
key: `grid-item-${panel.spec.id}`,
|
||||
key: `grid-item-${id ?? panel.spec.id}`,
|
||||
x: gridItem.x,
|
||||
y: yOverride ?? gridItem.y,
|
||||
width: gridItem.repeat?.direction === 'h' ? 24 : gridItem.width,
|
||||
@ -287,3 +292,21 @@ function buildGridItem(
|
||||
maxPerRow: gridItem.repeat?.maxPerRow,
|
||||
});
|
||||
}
|
||||
|
||||
export function deserializeGridItem(
|
||||
item: GridLayoutItemKind,
|
||||
elements: DashboardV2Spec['elements'],
|
||||
panelIdGenerator?: () => number
|
||||
): DashboardGridItem {
|
||||
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`);
|
||||
}
|
||||
|
||||
let id: number | undefined;
|
||||
if (panelIdGenerator) {
|
||||
id = panelIdGenerator();
|
||||
}
|
||||
return buildGridItem(item.spec, panel, undefined, id);
|
||||
}
|
||||
|
@ -31,45 +31,46 @@ export function serializeAutoGridLayout(layoutManager: AutoGridLayoutManager): D
|
||||
fillScreen: fillScreen === defaults.fillScreen ? undefined : fillScreen,
|
||||
...serializeAutoGridColumnWidth(columnWidth),
|
||||
...serializeAutoGridRowHeight(rowHeight),
|
||||
items: layout.state.children.map((child) => {
|
||||
if (!(child instanceof AutoGridItem)) {
|
||||
throw new Error('Expected AutoGridItem');
|
||||
}
|
||||
// For serialization we should retrieve the original element key
|
||||
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(child.state?.body);
|
||||
|
||||
const layoutItem: AutoGridLayoutItemKind = {
|
||||
kind: 'AutoGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: elementKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const conditionalRenderingRootGroup = child.state.conditionalRendering?.serialize();
|
||||
// Only serialize the conditional rendering if it has items
|
||||
if (conditionalRenderingRootGroup?.spec.items.length) {
|
||||
layoutItem.spec.conditionalRendering = conditionalRenderingRootGroup;
|
||||
}
|
||||
|
||||
if (child.state.variableName) {
|
||||
layoutItem.spec.repeat = {
|
||||
mode: 'variable',
|
||||
value: child.state.variableName,
|
||||
};
|
||||
}
|
||||
|
||||
return layoutItem;
|
||||
}),
|
||||
items: layout.state.children.map(serializeAutoGridItem),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeAutoGridItem(item: AutoGridItem): AutoGridLayoutItemKind {
|
||||
// For serialization we should retrieve the original element key
|
||||
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(item.state?.body);
|
||||
|
||||
const layoutItem: AutoGridLayoutItemKind = {
|
||||
kind: 'AutoGridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: elementKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const conditionalRenderingRootGroup = item.state.conditionalRendering?.serialize();
|
||||
// Only serialize the conditional rendering if it has items
|
||||
if (conditionalRenderingRootGroup?.spec.items.length) {
|
||||
layoutItem.spec.conditionalRendering = conditionalRenderingRootGroup;
|
||||
}
|
||||
|
||||
if (item.state.variableName) {
|
||||
layoutItem.spec.repeat = {
|
||||
mode: 'variable',
|
||||
value: item.state.variableName,
|
||||
};
|
||||
}
|
||||
|
||||
return layoutItem;
|
||||
}
|
||||
|
||||
export function deserializeAutoGridLayout(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements']
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): AutoGridLayoutManager {
|
||||
if (layout.kind !== 'AutoGridLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
@ -78,18 +79,7 @@ export function deserializeAutoGridLayout(
|
||||
const defaults = defaultAutoGridLayoutSpec();
|
||||
const { maxColumnCount, columnWidthMode, columnWidth, rowHeightMode, rowHeight, fillScreen } = layout.spec;
|
||||
|
||||
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`);
|
||||
}
|
||||
return new AutoGridItem({
|
||||
key: getGridItemKeyForPanelId(panel.spec.id),
|
||||
body: panel.kind === 'LibraryPanel' ? buildLibraryPanel(panel) : buildVizPanel(panel),
|
||||
variableName: item.spec.repeat?.value,
|
||||
conditionalRendering: getConditionalRendering(item),
|
||||
});
|
||||
});
|
||||
const children = layout.spec.items.map((item) => deserializeAutoGridItem(item, elements, panelIdGenerator));
|
||||
|
||||
const columnWidthCombined = columnWidthMode === 'custom' ? columnWidth : columnWidthMode;
|
||||
const rowHeightCombined = rowHeightMode === 'custom' ? rowHeight : rowHeightMode;
|
||||
@ -123,3 +113,24 @@ function serializeAutoGridRowHeight(rowHeight: AutoGridRowHeight) {
|
||||
rowHeight: typeof rowHeight === 'number' ? rowHeight : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function deserializeAutoGridItem(
|
||||
item: AutoGridLayoutItemKind,
|
||||
elements: DashboardV2Spec['elements'],
|
||||
panelIdGenerator?: () => number
|
||||
): AutoGridItem {
|
||||
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`);
|
||||
}
|
||||
let id: number | undefined;
|
||||
if (panelIdGenerator) {
|
||||
id = panelIdGenerator();
|
||||
}
|
||||
return new AutoGridItem({
|
||||
key: getGridItemKeyForPanelId(id ?? panel.spec.id),
|
||||
body: panel.kind === 'LibraryPanel' ? buildLibraryPanel(panel, id) : buildVizPanel(panel, id),
|
||||
variableName: item.spec.repeat?.value,
|
||||
conditionalRendering: getConditionalRendering(item),
|
||||
});
|
||||
}
|
||||
|
@ -12,65 +12,75 @@ export function serializeRowsLayout(layoutManager: RowsLayoutManager): Dashboard
|
||||
return {
|
||||
kind: 'RowsLayout',
|
||||
spec: {
|
||||
rows: layoutManager.state.rows.map((row) => {
|
||||
const layout = row.state.layout.serialize();
|
||||
const rowKind: RowsLayoutRowKind = {
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: row.state.title,
|
||||
collapse: row.state.collapse,
|
||||
layout: layout,
|
||||
fillScreen: row.state.fillScreen,
|
||||
hideHeader: row.state.hideHeader,
|
||||
},
|
||||
};
|
||||
|
||||
const conditionalRenderingRootGroup = row.state.conditionalRendering?.serialize();
|
||||
// Only serialize the conditional rendering if it has items
|
||||
if (conditionalRenderingRootGroup?.spec.items.length) {
|
||||
rowKind.spec.conditionalRendering = conditionalRenderingRootGroup;
|
||||
}
|
||||
|
||||
if (row.state.$behaviors) {
|
||||
for (const behavior of row.state.$behaviors) {
|
||||
if (behavior instanceof RowItemRepeaterBehavior) {
|
||||
if (rowKind.spec.repeat) {
|
||||
throw new Error('Multiple repeaters are not supported');
|
||||
}
|
||||
rowKind.spec.repeat = { value: behavior.state.variableName, mode: 'variable' };
|
||||
}
|
||||
}
|
||||
}
|
||||
return rowKind;
|
||||
}),
|
||||
rows: layoutManager.state.rows.map(serializeRow),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeRow(row: RowItem): RowsLayoutRowKind {
|
||||
const layout = row.state.layout.serialize();
|
||||
const rowKind: RowsLayoutRowKind = {
|
||||
kind: 'RowsLayoutRow',
|
||||
spec: {
|
||||
title: row.state.title,
|
||||
collapse: row.state.collapse,
|
||||
layout: layout,
|
||||
fillScreen: row.state.fillScreen,
|
||||
hideHeader: row.state.hideHeader,
|
||||
},
|
||||
};
|
||||
|
||||
const conditionalRenderingRootGroup = row.state.conditionalRendering?.serialize();
|
||||
// Only serialize the conditional rendering if it has items
|
||||
if (conditionalRenderingRootGroup?.spec.items.length) {
|
||||
rowKind.spec.conditionalRendering = conditionalRenderingRootGroup;
|
||||
}
|
||||
|
||||
if (row.state.$behaviors) {
|
||||
for (const behavior of row.state.$behaviors) {
|
||||
if (behavior instanceof RowItemRepeaterBehavior) {
|
||||
if (rowKind.spec.repeat) {
|
||||
throw new Error('Multiple repeaters are not supported');
|
||||
}
|
||||
rowKind.spec.repeat = { value: behavior.state.variableName, mode: 'variable' };
|
||||
}
|
||||
}
|
||||
}
|
||||
return rowKind;
|
||||
}
|
||||
|
||||
export function deserializeRowsLayout(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): RowsLayoutManager {
|
||||
if (layout.kind !== 'RowsLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
}
|
||||
const rows = layout.spec.rows.map((row) => {
|
||||
const layout = row.spec.layout;
|
||||
const behaviors: SceneObject[] = [];
|
||||
if (row.spec.repeat) {
|
||||
behaviors.push(new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value }));
|
||||
}
|
||||
|
||||
return new RowItem({
|
||||
title: row.spec.title,
|
||||
collapse: row.spec.collapse,
|
||||
hideHeader: row.spec.hideHeader,
|
||||
fillScreen: row.spec.fillScreen,
|
||||
$behaviors: behaviors,
|
||||
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload),
|
||||
conditionalRendering: getConditionalRendering(row),
|
||||
});
|
||||
});
|
||||
const rows = layout.spec.rows.map((row) => deserializeRow(row, elements, preload, panelIdGenerator));
|
||||
return new RowsLayoutManager({ rows });
|
||||
}
|
||||
|
||||
export function deserializeRow(
|
||||
row: RowsLayoutRowKind,
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): RowItem {
|
||||
const layout = row.spec.layout;
|
||||
const behaviors: SceneObject[] = [];
|
||||
if (row.spec.repeat) {
|
||||
behaviors.push(new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value }));
|
||||
}
|
||||
|
||||
return new RowItem({
|
||||
title: row.spec.title,
|
||||
collapse: row.spec.collapse,
|
||||
hideHeader: row.spec.hideHeader,
|
||||
fillScreen: row.spec.fillScreen,
|
||||
$behaviors: behaviors,
|
||||
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload, panelIdGenerator),
|
||||
conditionalRendering: getConditionalRendering(row),
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { DashboardV2Spec, TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
|
||||
import { TabItem } from '../../scene/layout-tabs/TabItem';
|
||||
import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager';
|
||||
@ -9,16 +9,18 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard
|
||||
return {
|
||||
kind: 'TabsLayout',
|
||||
spec: {
|
||||
tabs: layoutManager.state.tabs.map((tab) => {
|
||||
const layout = tab.state.layout.serialize();
|
||||
return {
|
||||
kind: 'TabsLayoutTab',
|
||||
spec: {
|
||||
title: tab.state.title,
|
||||
layout: layout,
|
||||
},
|
||||
};
|
||||
}),
|
||||
tabs: layoutManager.state.tabs.map(serializeTab),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeTab(tab: TabItem): TabsLayoutTabKind {
|
||||
const layout = tab.state.layout.serialize();
|
||||
return {
|
||||
kind: 'TabsLayoutTab',
|
||||
spec: {
|
||||
title: tab.state.title,
|
||||
layout: layout,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -26,17 +28,29 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard
|
||||
export function deserializeTabsLayout(
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): TabsLayoutManager {
|
||||
if (layout.kind !== 'TabsLayout') {
|
||||
throw new Error('Invalid layout kind');
|
||||
}
|
||||
|
||||
const tabs = layout.spec.tabs.map((tab) => {
|
||||
const layout = tab.spec.layout;
|
||||
return new TabItem({
|
||||
title: tab.spec.title,
|
||||
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload),
|
||||
});
|
||||
return deserializeTab(tab, elements, preload, panelIdGenerator);
|
||||
});
|
||||
|
||||
return new TabsLayoutManager({ tabs });
|
||||
}
|
||||
|
||||
export function deserializeTab(
|
||||
tab: TabsLayoutTabKind,
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
): TabItem {
|
||||
const layout = tab.spec.layout;
|
||||
return new TabItem({
|
||||
title: tab.spec.title,
|
||||
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload, panelIdGenerator),
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ interface LayoutSerializerRegistryItem extends RegistryItem {
|
||||
deserialize: (
|
||||
layout: DashboardV2Spec['layout'],
|
||||
elements: DashboardV2Spec['elements'],
|
||||
preload: boolean
|
||||
preload: boolean,
|
||||
panelIdGenerator?: () => number
|
||||
) => DashboardLayoutManager;
|
||||
}
|
||||
|
||||
|
@ -25,18 +25,22 @@ import { ConditionalRendering } from '../../conditional-rendering/ConditionalRen
|
||||
import { ConditionalRenderingGroup } from '../../conditional-rendering/ConditionalRenderingGroup';
|
||||
import { conditionalRenderingSerializerRegistry } from '../../conditional-rendering/serializers';
|
||||
import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
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 { AutoGridItem } from '../../scene/layout-responsive-grid/ResponsiveGridItem';
|
||||
import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext';
|
||||
import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager';
|
||||
import { getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { createElements, vizPanelToSchemaV2 } from '../transformSceneToSaveModelSchemaV2';
|
||||
import { transformMappingsToV1 } from '../transformToV1TypesUtils';
|
||||
|
||||
export function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
export function buildVizPanel(panel: PanelKind, id?: number): VizPanel {
|
||||
const titleItems: SceneObject[] = [];
|
||||
|
||||
if (config.featureToggles.angularDeprecationUI) {
|
||||
@ -56,7 +60,7 @@ export function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: getVizPanelKeyForPanelId(panel.spec.id),
|
||||
key: getVizPanelKeyForPanelId(id ?? panel.spec.id),
|
||||
title: panel.spec.title?.substring(0, 5000),
|
||||
description: panel.spec.description,
|
||||
pluginId: panel.spec.vizConfig.kind,
|
||||
@ -90,7 +94,7 @@ export function buildVizPanel(panel: PanelKind): VizPanel {
|
||||
return new VizPanel(vizPanelState);
|
||||
}
|
||||
|
||||
export function buildLibraryPanel(panel: LibraryPanelKind): VizPanel {
|
||||
export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPanel {
|
||||
const titleItems: SceneObject[] = [];
|
||||
|
||||
if (config.featureToggles.angularDeprecationUI) {
|
||||
@ -107,7 +111,7 @@ export function buildLibraryPanel(panel: LibraryPanelKind): VizPanel {
|
||||
titleItems.push(new PanelNotices());
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: getVizPanelKeyForPanelId(panel.spec.id),
|
||||
key: getVizPanelKeyForPanelId(id ?? panel.spec.id),
|
||||
titleItems,
|
||||
$behaviors: [
|
||||
new LibraryPanelBehavior({
|
||||
@ -242,3 +246,19 @@ export function getConditionalRendering(item: RowsLayoutRowKind | AutoGridLayout
|
||||
|
||||
return new ConditionalRendering({ rootGroup: rootGroup });
|
||||
}
|
||||
|
||||
export function getElements(layout: DashboardLayoutManager, scene: DashboardScene): DashboardV2Spec['elements'] {
|
||||
const panels = layout.getVizPanels();
|
||||
const dsReferencesMapping = scene.serializer.getDSReferencesMapping();
|
||||
const panelsArray = panels.map((vizPanel) => {
|
||||
return vizPanelToSchemaV2(vizPanel, dsReferencesMapping);
|
||||
});
|
||||
return createElements(panelsArray, scene);
|
||||
}
|
||||
|
||||
export function getElement(
|
||||
gridItem: AutoGridItem | DashboardGridItem,
|
||||
scene: DashboardScene
|
||||
): DashboardV2Spec['elements'] {
|
||||
return createElements([vizPanelToSchemaV2(gridItem.state.body, scene.serializer.getDSReferencesMapping())], scene);
|
||||
}
|
||||
|
@ -348,7 +348,7 @@ function getVizPanelQueryOptions(vizPanel: VizPanel): QueryOptionsSpec {
|
||||
return queryOptions;
|
||||
}
|
||||
|
||||
function createElements(panels: Element[], scene: DashboardScene): Record<string, Element> {
|
||||
export function createElements(panels: Element[], scene: DashboardScene): Record<string, Element> {
|
||||
return panels.reduce<Record<string, Element>>((elements, panel) => {
|
||||
let elementKey = scene.serializer.getElementIdForPanel(panel.spec.id);
|
||||
elements[elementKey!] = panel;
|
||||
|
@ -1417,7 +1417,10 @@
|
||||
"group-into-tab": "Group into tab",
|
||||
"group-panels": "Group panels",
|
||||
"new-row": "New row",
|
||||
"new-tab": "New tab"
|
||||
"new-tab": "New tab",
|
||||
"paste-panel": "Paste panel",
|
||||
"paste-row": "Paste row",
|
||||
"paste-tab": "Paste tab"
|
||||
},
|
||||
"conditional-rendering": {
|
||||
"data": {
|
||||
@ -1523,10 +1526,6 @@
|
||||
"heading": "Panel",
|
||||
"title": "A container for visualizations and other content"
|
||||
},
|
||||
"paste-panel": {
|
||||
"heading": "Paste panel",
|
||||
"title": "Paste a panel from the clipboard"
|
||||
},
|
||||
"row": {
|
||||
"heading": "Row",
|
||||
"title": "A group of panels with an optional header"
|
||||
|
Reference in New Issue
Block a user