Dashboard: Switch back to prev layout restores state (#102304)
This commit is contained in:
@@ -70,6 +70,7 @@ import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './ang
|
||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { LayoutRestorer } from './layouts-shared/LayoutRestorer';
|
||||
import { addNewRowTo, addNewTabTo } from './layouts-shared/addNew';
|
||||
import { DashboardLayoutManager } from './types/DashboardLayoutManager';
|
||||
import { LayoutParent } from './types/LayoutParent';
|
||||
@@ -174,6 +175,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
DashboardMeta | DashboardWithAccessInfo<DashboardV2Spec>['metadata']
|
||||
>;
|
||||
|
||||
private _layoutRestorer = new LayoutRestorer();
|
||||
|
||||
public constructor(state: Partial<DashboardSceneState>, serializerVersion: 'v1' | 'v2' = 'v1') {
|
||||
super({
|
||||
title: 'Dashboard',
|
||||
@@ -613,8 +616,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
}
|
||||
|
||||
public switchLayout(layout: DashboardLayoutManager) {
|
||||
this.setState({ body: layout });
|
||||
layout.activateRepeaters?.();
|
||||
this.setState({ body: this._layoutRestorer.getLayout(layout, this.state.body) });
|
||||
this.state.body.activateRepeaters?.();
|
||||
}
|
||||
|
||||
public getLayout(): DashboardLayoutManager {
|
||||
|
||||
+1
-1
@@ -426,7 +426,7 @@ export class DefaultGridLayoutManager
|
||||
|
||||
currentX += panelWidth;
|
||||
|
||||
if (currentX + panelWidth >= GRID_COLUMN_COUNT) {
|
||||
if (currentX + panelWidth > GRID_COLUMN_COUNT) {
|
||||
currentX = 0;
|
||||
currentY += panelHeight;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { LayoutRestorer } from '../layouts-shared/LayoutRestorer';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
@@ -36,6 +37,7 @@ export class RowItem
|
||||
});
|
||||
|
||||
public readonly isEditableDashboardElement = true;
|
||||
private _layoutRestorer = new LayoutRestorer();
|
||||
|
||||
public constructor(state?: Partial<RowItemState>) {
|
||||
super({
|
||||
@@ -71,7 +73,7 @@ export class RowItem
|
||||
}
|
||||
|
||||
public switchLayout(layout: DashboardLayoutManager) {
|
||||
this.setState({ layout });
|
||||
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { LayoutRestorer } from '../layouts-shared/LayoutRestorer';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
@@ -30,6 +31,7 @@ export class TabItem
|
||||
});
|
||||
|
||||
public readonly isEditableDashboardElement = true;
|
||||
private _layoutRestorer = new LayoutRestorer();
|
||||
|
||||
constructor(state?: Partial<TabItemState>) {
|
||||
super({
|
||||
@@ -52,7 +54,7 @@ export class TabItem
|
||||
}
|
||||
|
||||
public switchLayout(layout: DashboardLayoutManager) {
|
||||
this.setState({ layout });
|
||||
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
|
||||
+37
-34
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { RadioButtonDot, Stack, useStyles2, Text } from '@grafana/ui';
|
||||
@@ -20,9 +20,19 @@ export interface Props {
|
||||
export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
const isGridLayout = layoutManager.descriptor.isGridLayout;
|
||||
const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onChangeLayout = useCallback(
|
||||
(newLayout: LayoutRegistryItem) => {
|
||||
const layoutParent = layoutManager.parent;
|
||||
|
||||
if (layoutParent && isLayoutParent(layoutParent)) {
|
||||
layoutParent.switchLayout(newLayout.createFromLayout(layoutManager));
|
||||
}
|
||||
},
|
||||
[layoutManager]
|
||||
);
|
||||
|
||||
return (
|
||||
<div role="radiogroup" className={styles.radioGroup}>
|
||||
{options.map((opt) => {
|
||||
@@ -30,11 +40,10 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
case 'rows-layout':
|
||||
return (
|
||||
<LayoutRadioButton
|
||||
label={opt.name}
|
||||
id={opt.id}
|
||||
description={opt.description!}
|
||||
item={opt}
|
||||
isSelected={layoutManager.descriptor.id === opt.id}
|
||||
onSelect={() => changeLayoutTo(layoutManager, opt)}
|
||||
onSelect={onChangeLayout}
|
||||
key={opt.id}
|
||||
>
|
||||
<div className={styles.rowsLayoutViz}>
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
@@ -53,11 +62,10 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
case 'tabs-layout':
|
||||
return (
|
||||
<LayoutRadioButton
|
||||
label={opt.name}
|
||||
id={opt.id}
|
||||
description={opt.description!}
|
||||
item={opt}
|
||||
isSelected={layoutManager.descriptor.id === opt.id}
|
||||
onSelect={() => changeLayoutTo(layoutManager, opt)}
|
||||
onSelect={onChangeLayout}
|
||||
key={opt.id}
|
||||
>
|
||||
<Stack direction="column" gap={0.5} height={'100%'}>
|
||||
<div className={styles.tabsBar}>
|
||||
@@ -78,11 +86,10 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
case 'responsive-grid':
|
||||
return (
|
||||
<LayoutRadioButton
|
||||
label={opt.name}
|
||||
id={opt.id}
|
||||
description={opt.description!}
|
||||
item={opt}
|
||||
isSelected={layoutManager.descriptor.id === opt.id}
|
||||
onSelect={() => changeLayoutTo(layoutManager, opt)}
|
||||
onSelect={onChangeLayout}
|
||||
key={opt.id}
|
||||
>
|
||||
<div className={styles.autoGridViz}>
|
||||
<GridCell />
|
||||
@@ -96,11 +103,10 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
default:
|
||||
return (
|
||||
<LayoutRadioButton
|
||||
label={opt.name}
|
||||
id={opt.id}
|
||||
description={opt.description!}
|
||||
item={opt}
|
||||
isSelected={layoutManager.descriptor.id === opt.id}
|
||||
onSelect={() => changeLayoutTo(layoutManager, opt)}
|
||||
onSelect={onChangeLayout}
|
||||
key={opt.id}
|
||||
>
|
||||
<div className={styles.customGridViz}>
|
||||
<GridCell colSpan={2} />
|
||||
@@ -120,15 +126,13 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
|
||||
}
|
||||
|
||||
interface LayoutRadioButtonProps {
|
||||
label: string;
|
||||
id: string;
|
||||
description: string;
|
||||
item: LayoutRegistryItem;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onSelect: (item: LayoutRegistryItem) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function LayoutRadioButton({ label, id, description, isSelected, children, onSelect }: LayoutRadioButtonProps) {
|
||||
function LayoutRadioButton({ item, isSelected, children, onSelect }: LayoutRadioButtonProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
@@ -136,20 +140,26 @@ function LayoutRadioButton({ label, id, description, isSelected, children, onSel
|
||||
// label (as the RadioButtonDot has a label element and they can't nest)
|
||||
<div className={styles.radioButtonOuter}>
|
||||
<label
|
||||
htmlFor={`layout-${id}`}
|
||||
htmlFor={`layout-${item.id}`}
|
||||
tabIndex={0}
|
||||
className={cx(styles.radioButton, isSelected && styles.radioButtonActive)}
|
||||
>
|
||||
{children}
|
||||
<Stack direction="column" gap={1} justifyContent="space-between" grow={1}>
|
||||
<Text weight="medium">{label}</Text>
|
||||
<Text weight="medium">{item.name}</Text>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{description}
|
||||
{item.description!}
|
||||
</Text>
|
||||
</Stack>
|
||||
</label>
|
||||
<div className={styles.radioDot}>
|
||||
<RadioButtonDot id={`layout-${id}`} name={'layout'} label={<></>} onChange={onSelect} checked={isSelected} />
|
||||
<RadioButtonDot
|
||||
id={`layout-${item.id}`}
|
||||
name={'layout'}
|
||||
label={<></>}
|
||||
onChange={() => onSelect(item)}
|
||||
checked={isSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -191,13 +201,6 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
|
||||
}, [layoutManager]);
|
||||
}
|
||||
|
||||
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) {
|
||||
const layoutParent = currentLayout.parent;
|
||||
if (layoutParent && isLayoutParent(layoutParent)) {
|
||||
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout));
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
radioButtonOuter: css({
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { vizPanelToSchemaV2 } from '../../serialization/transformSceneToSaveModelSchemaV2';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
export class LayoutRestorer {
|
||||
private layoutMap: Record<string, DashboardLayoutManager> = {};
|
||||
|
||||
public getLayout(
|
||||
newLayout: DashboardLayoutManager,
|
||||
currentLayout: DashboardLayoutManager
|
||||
): DashboardLayoutManager | undefined {
|
||||
// If we have an old version of this layout and panels are the same we can reuse it
|
||||
const prevLayout = this.layoutMap[newLayout.descriptor.id];
|
||||
if (prevLayout) {
|
||||
const oldPanelSchema = prevLayout.getVizPanels().map(vizPanelToSchemaV2);
|
||||
const newPanelSchema = newLayout.getVizPanels().map(vizPanelToSchemaV2);
|
||||
if (JSON.stringify(oldPanelSchema) === JSON.stringify(newPanelSchema)) {
|
||||
return prevLayout;
|
||||
}
|
||||
}
|
||||
|
||||
this.layoutMap[currentLayout.descriptor.id] = currentLayout;
|
||||
|
||||
return newLayout;
|
||||
}
|
||||
}
|
||||
+81
-79
@@ -143,88 +143,90 @@ function getLiveNow(state: DashboardSceneState) {
|
||||
function getElements(scene: DashboardScene) {
|
||||
const panels = scene.state.body.getVizPanels() ?? [];
|
||||
|
||||
const panelsArray = panels.map((vizPanel: VizPanel) => {
|
||||
if (isLibraryPanel(vizPanel)) {
|
||||
const behavior = getLibraryPanelBehavior(vizPanel)!;
|
||||
const elementSpec: LibraryPanelKind = {
|
||||
kind: 'LibraryPanel',
|
||||
spec: {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
title: vizPanel.state.title,
|
||||
libraryPanel: {
|
||||
uid: behavior.state.uid,
|
||||
name: behavior.state.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
return elementSpec;
|
||||
} else {
|
||||
// Handle type conversion for color mode
|
||||
const rawColor = vizPanel.state.fieldConfig.defaults.color;
|
||||
let color: FieldColor | undefined;
|
||||
|
||||
if (rawColor) {
|
||||
const convertedMode = colorIdEnumToColorIdV2(rawColor.mode);
|
||||
|
||||
if (convertedMode) {
|
||||
color = {
|
||||
...rawColor,
|
||||
mode: convertedMode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove null from the defaults because schema V2 doesn't support null for these fields
|
||||
const decimals = vizPanel.state.fieldConfig.defaults.decimals ?? undefined;
|
||||
const min = vizPanel.state.fieldConfig.defaults.min ?? undefined;
|
||||
const max = vizPanel.state.fieldConfig.defaults.max ?? undefined;
|
||||
|
||||
const defaults: FieldConfig = Object.fromEntries(
|
||||
Object.entries({
|
||||
...vizPanel.state.fieldConfig.defaults,
|
||||
decimals,
|
||||
min,
|
||||
max,
|
||||
color,
|
||||
}).filter(([_, value]) => value !== undefined)
|
||||
);
|
||||
|
||||
const vizFieldConfig: FieldConfigSource = {
|
||||
...vizPanel.state.fieldConfig,
|
||||
defaults,
|
||||
};
|
||||
|
||||
const elementSpec: PanelKind = {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
title: vizPanel.state.title,
|
||||
description: vizPanel.state.description ?? '',
|
||||
links: getPanelLinks(vizPanel),
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: getVizPanelQueries(vizPanel),
|
||||
transformations: getVizPanelTransformations(vizPanel),
|
||||
queryOptions: getVizPanelQueryOptions(vizPanel),
|
||||
},
|
||||
},
|
||||
vizConfig: {
|
||||
kind: vizPanel.state.pluginId,
|
||||
spec: {
|
||||
pluginVersion: vizPanel.state.pluginVersion ?? '',
|
||||
options: vizPanel.state.options,
|
||||
fieldConfig: vizFieldConfig ?? defaultFieldConfigSource(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return elementSpec;
|
||||
}
|
||||
});
|
||||
const panelsArray = panels.map(vizPanelToSchemaV2);
|
||||
return createElements(panelsArray, scene);
|
||||
}
|
||||
|
||||
export function vizPanelToSchemaV2(vizPanel: VizPanel): PanelKind | LibraryPanelKind {
|
||||
if (isLibraryPanel(vizPanel)) {
|
||||
const behavior = getLibraryPanelBehavior(vizPanel)!;
|
||||
const elementSpec: LibraryPanelKind = {
|
||||
kind: 'LibraryPanel',
|
||||
spec: {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
title: vizPanel.state.title,
|
||||
libraryPanel: {
|
||||
uid: behavior.state.uid,
|
||||
name: behavior.state.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
return elementSpec;
|
||||
}
|
||||
|
||||
// Handle type conversion for color mode
|
||||
const rawColor = vizPanel.state.fieldConfig.defaults.color;
|
||||
let color: FieldColor | undefined;
|
||||
|
||||
if (rawColor) {
|
||||
const convertedMode = colorIdEnumToColorIdV2(rawColor.mode);
|
||||
|
||||
if (convertedMode) {
|
||||
color = {
|
||||
...rawColor,
|
||||
mode: convertedMode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove null from the defaults because schema V2 doesn't support null for these fields
|
||||
const decimals = vizPanel.state.fieldConfig.defaults.decimals ?? undefined;
|
||||
const min = vizPanel.state.fieldConfig.defaults.min ?? undefined;
|
||||
const max = vizPanel.state.fieldConfig.defaults.max ?? undefined;
|
||||
|
||||
const defaults: FieldConfig = Object.fromEntries(
|
||||
Object.entries({
|
||||
...vizPanel.state.fieldConfig.defaults,
|
||||
decimals,
|
||||
min,
|
||||
max,
|
||||
color,
|
||||
}).filter(([_, value]) => value !== undefined)
|
||||
);
|
||||
|
||||
const vizFieldConfig: FieldConfigSource = {
|
||||
...vizPanel.state.fieldConfig,
|
||||
defaults,
|
||||
};
|
||||
|
||||
const elementSpec: PanelKind = {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: getPanelIdForVizPanel(vizPanel),
|
||||
title: vizPanel.state.title,
|
||||
description: vizPanel.state.description ?? '',
|
||||
links: getPanelLinks(vizPanel),
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: getVizPanelQueries(vizPanel),
|
||||
transformations: getVizPanelTransformations(vizPanel),
|
||||
queryOptions: getVizPanelQueryOptions(vizPanel),
|
||||
},
|
||||
},
|
||||
vizConfig: {
|
||||
kind: vizPanel.state.pluginId,
|
||||
spec: {
|
||||
pluginVersion: vizPanel.state.pluginVersion ?? '',
|
||||
options: vizPanel.state.options,
|
||||
fieldConfig: vizFieldConfig ?? defaultFieldConfigSource(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return elementSpec;
|
||||
}
|
||||
|
||||
function getPanelLinks(panel: VizPanel): DataLink[] {
|
||||
const vizLinks = dashboardSceneGraph.getPanelLinks(panel);
|
||||
if (vizLinks) {
|
||||
|
||||
Reference in New Issue
Block a user