mirror of https://github.com/grafana/grafana
Dashboards: Rows layout & editing (#96895)
* Dashboard: Panel edit and support for more layout items * It's working * Fix discard issue * remove unused file * Update * Editing for responsive grid items now work * Update * Update * Update * WIP rows * progres * Progress * Progress * Focus selection works * Update * Update * Progress * Update * Editing rows work * Row editing works * fix delete * Update * Row options fix * Fix selecting rows * Update * update * Update * Update * Remove cog icon button * add import to toolbar * Update * Updatepull/97717/head
parent
d762a96436
commit
71b8f487e0
@ -0,0 +1,130 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes'; |
||||
import { Button } from '@grafana/ui'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
||||
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; |
||||
|
||||
import { |
||||
PanelBackgroundSwitch, |
||||
PanelDescriptionTextArea, |
||||
PanelFrameTitleInput, |
||||
} from '../panel-edit/getPanelFrameOptions'; |
||||
import { EditableDashboardElement, isDashboardLayoutItem } from '../scene/types'; |
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; |
||||
|
||||
export class VizPanelEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement { |
||||
public isEditableDashboardElement: true = true; |
||||
|
||||
private getPanel(): VizPanel { |
||||
const panel = this.parent; |
||||
|
||||
if (!(panel instanceof VizPanel)) { |
||||
throw new Error('VizPanelEditPaneBehavior must have a VizPanel parent'); |
||||
} |
||||
|
||||
return panel; |
||||
} |
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { |
||||
const panel = this.getPanel(); |
||||
const layoutElement = panel.parent!; |
||||
|
||||
const panelOptions = useMemo(() => { |
||||
return new OptionsPaneCategoryDescriptor({ |
||||
title: 'Panel options', |
||||
id: 'panel-options', |
||||
isOpenDefault: true, |
||||
}) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Title', |
||||
value: panel.state.title, |
||||
popularRank: 1, |
||||
render: function renderTitle() { |
||||
return <PanelFrameTitleInput panel={panel} />; |
||||
}, |
||||
}) |
||||
) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Description', |
||||
value: panel.state.description, |
||||
render: function renderDescription() { |
||||
return <PanelDescriptionTextArea panel={panel} />; |
||||
}, |
||||
}) |
||||
) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Transparent background', |
||||
render: function renderTransparent() { |
||||
return <PanelBackgroundSwitch panel={panel} />; |
||||
}, |
||||
}) |
||||
); |
||||
}, [panel]); |
||||
|
||||
const layoutCategory = useMemo(() => { |
||||
if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) { |
||||
return layoutElement.getOptions(); |
||||
} |
||||
return undefined; |
||||
}, [layoutElement]); |
||||
|
||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState(); |
||||
const dataProvider = sceneGraph.getData(panel); |
||||
const { data } = dataProvider.useState(); |
||||
|
||||
const visualizationOptions = useMemo(() => { |
||||
const plugin = panel.getPlugin(); |
||||
if (!plugin) { |
||||
return []; |
||||
} |
||||
|
||||
return getVisualizationOptions2({ |
||||
panel, |
||||
data, |
||||
plugin: plugin, |
||||
eventBus: panel.getPanelContext().eventBus, |
||||
instanceState: _pluginInstanceState, |
||||
}); |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, panel, options, fieldConfig, _pluginInstanceState]); |
||||
|
||||
const categories = [panelOptions]; |
||||
if (layoutCategory) { |
||||
categories.push(layoutCategory); |
||||
} |
||||
|
||||
categories.push(...visualizationOptions); |
||||
|
||||
return categories; |
||||
} |
||||
|
||||
public getTypeName(): string { |
||||
return 'Panel'; |
||||
} |
||||
|
||||
public onDelete = () => { |
||||
const layout = dashboardSceneGraph.getLayoutManagerFor(this); |
||||
layout.removePanel(this.getPanel()); |
||||
}; |
||||
|
||||
public renderActions(): React.ReactNode { |
||||
return ( |
||||
<> |
||||
<Button size="sm" variant="secondary"> |
||||
Edit |
||||
</Button> |
||||
<Button size="sm" variant="secondary"> |
||||
Copy |
||||
</Button> |
||||
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete}> |
||||
Delete |
||||
</Button> |
||||
</> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,242 @@ |
||||
import { css, cx } from '@emotion/css'; |
||||
import { useMemo, useRef } from 'react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
import { SceneObjectState, SceneObjectBase, SceneComponentProps, sceneGraph } from '@grafana/scenes'; |
||||
import { Button, Icon, Input, RadioButtonGroup, Switch, useStyles2 } from '@grafana/ui'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
||||
|
||||
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils'; |
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; |
||||
import { DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types'; |
||||
|
||||
import { RowsLayoutManager } from './RowsLayoutManager'; |
||||
|
||||
export interface RowItemState extends SceneObjectState { |
||||
layout: DashboardLayoutManager; |
||||
title?: string; |
||||
isCollapsed?: boolean; |
||||
isHeaderHidden?: boolean; |
||||
height?: 'expand' | 'min'; |
||||
} |
||||
|
||||
export class RowItem extends SceneObjectBase<RowItemState> implements LayoutParent, EditableDashboardElement { |
||||
public isEditableDashboardElement: true = true; |
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { |
||||
const row = this; |
||||
|
||||
const rowOptions = useMemo(() => { |
||||
return new OptionsPaneCategoryDescriptor({ |
||||
title: 'Row options', |
||||
id: 'row-options', |
||||
isOpenDefault: true, |
||||
}) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Title', |
||||
render: () => <RowTitleInput row={row} />, |
||||
}) |
||||
) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Height', |
||||
render: () => <RowHeightSelect row={row} />, |
||||
}) |
||||
) |
||||
.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Hide row header', |
||||
render: () => <RowHeaderSwitch row={row} />, |
||||
}) |
||||
); |
||||
}, [row]); |
||||
|
||||
const { layout } = this.useState(); |
||||
const layoutOptions = useLayoutCategory(layout); |
||||
|
||||
return [rowOptions, layoutOptions]; |
||||
} |
||||
|
||||
public getTypeName(): string { |
||||
return 'Row'; |
||||
} |
||||
|
||||
public onDelete = () => { |
||||
const layout = sceneGraph.getAncestor(this, RowsLayoutManager); |
||||
layout.removeRow(this); |
||||
}; |
||||
|
||||
public renderActions(): React.ReactNode { |
||||
return ( |
||||
<> |
||||
<Button size="sm" variant="secondary"> |
||||
Copy |
||||
</Button> |
||||
<Button size="sm" variant="primary" onClick={this.onAddPanel} fill="outline"> |
||||
Add panel |
||||
</Button> |
||||
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete}> |
||||
Delete |
||||
</Button> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
public getLayout(): DashboardLayoutManager { |
||||
return this.state.layout; |
||||
} |
||||
|
||||
public switchLayout(layout: DashboardLayoutManager): void { |
||||
this.setState({ layout }); |
||||
} |
||||
|
||||
public onCollapseToggle = () => { |
||||
this.setState({ isCollapsed: !this.state.isCollapsed }); |
||||
}; |
||||
|
||||
public onAddPanel = () => { |
||||
const vizPanel = getDefaultVizPanel(); |
||||
this.state.layout.addPanel(vizPanel); |
||||
}; |
||||
|
||||
public onEdit = () => { |
||||
const dashboard = getDashboardSceneFor(this); |
||||
dashboard.state.editPane.selectObject(this); |
||||
}; |
||||
|
||||
public static Component = ({ model }: SceneComponentProps<RowItem>) => { |
||||
const { layout, title, isCollapsed, height = 'expand' } = model.useState(); |
||||
const { isEditing } = getDashboardSceneFor(model).useState(); |
||||
const styles = useStyles2(getStyles); |
||||
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); |
||||
const ref = useRef<HTMLDivElement>(null); |
||||
const shouldGrow = !isCollapsed && height === 'expand'; |
||||
|
||||
return ( |
||||
<div |
||||
className={cx(styles.wrapper, isCollapsed && styles.wrapperCollapsed, shouldGrow && styles.wrapperGrow)} |
||||
ref={ref} |
||||
> |
||||
<div className={styles.rowHeader}> |
||||
<button |
||||
onClick={model.onCollapseToggle} |
||||
className={styles.rowTitleButton} |
||||
aria-label={isCollapsed ? 'Expand row' : 'Collapse row'} |
||||
data-testid={selectors.components.DashboardRow.title(titleInterpolated)} |
||||
> |
||||
<Icon name={isCollapsed ? 'angle-right' : 'angle-down'} /> |
||||
<span className={styles.rowTitle} role="heading"> |
||||
{titleInterpolated} |
||||
</span> |
||||
</button> |
||||
{isEditing && <Button icon="pen" variant="secondary" size="sm" fill="text" onClick={() => model.onEdit()} />} |
||||
</div> |
||||
{!isCollapsed && <layout.Component model={layout} />} |
||||
</div> |
||||
); |
||||
}; |
||||
} |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
rowHeader: css({ |
||||
width: '100%', |
||||
display: 'flex', |
||||
gap: theme.spacing(1), |
||||
padding: theme.spacing(0, 0, 0.5, 0), |
||||
margin: theme.spacing(0, 0, 1, 0), |
||||
alignItems: 'center', |
||||
|
||||
'&:hover, &:focus-within': { |
||||
'& > div': { |
||||
opacity: 1, |
||||
}, |
||||
}, |
||||
|
||||
'& > div': { |
||||
marginBottom: 0, |
||||
marginRight: theme.spacing(1), |
||||
}, |
||||
}), |
||||
rowTitleButton: css({ |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
cursor: 'pointer', |
||||
background: 'transparent', |
||||
border: 'none', |
||||
minWidth: 0, |
||||
gap: theme.spacing(1), |
||||
}), |
||||
rowTitle: css({ |
||||
fontSize: theme.typography.h5.fontSize, |
||||
fontWeight: theme.typography.fontWeightMedium, |
||||
whiteSpace: 'nowrap', |
||||
overflow: 'hidden', |
||||
textOverflow: 'ellipsis', |
||||
maxWidth: '100%', |
||||
flexGrow: 1, |
||||
minWidth: 0, |
||||
}), |
||||
wrapper: css({ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
width: '100%', |
||||
}), |
||||
wrapperGrow: css({ |
||||
flexGrow: 1, |
||||
}), |
||||
wrapperCollapsed: css({ |
||||
flexGrow: 0, |
||||
borderBottom: `1px solid ${theme.colors.border.weak}`, |
||||
}), |
||||
rowActions: css({ |
||||
display: 'flex', |
||||
opacity: 0, |
||||
}), |
||||
}; |
||||
} |
||||
|
||||
export function RowTitleInput({ row }: { row: RowItem }) { |
||||
const { title } = row.useState(); |
||||
|
||||
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />; |
||||
} |
||||
|
||||
export function RowHeaderSwitch({ row }: { row: RowItem }) { |
||||
const { isHeaderHidden } = row.useState(); |
||||
|
||||
return ( |
||||
<Switch |
||||
value={isHeaderHidden} |
||||
onChange={() => { |
||||
row.setState({ |
||||
isHeaderHidden: !row.state.isHeaderHidden, |
||||
}); |
||||
}} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
export function RowHeightSelect({ row }: { row: RowItem }) { |
||||
const { height = 'expand' } = row.useState(); |
||||
|
||||
const options = [ |
||||
{ label: 'Expand', value: 'expand' as const }, |
||||
{ label: 'Min', value: 'min' as const }, |
||||
]; |
||||
|
||||
return ( |
||||
<RadioButtonGroup |
||||
options={options} |
||||
value={height} |
||||
onChange={(option) => |
||||
row.setState({ |
||||
height: option, |
||||
}) |
||||
} |
||||
/> |
||||
); |
||||
} |
@ -0,0 +1,113 @@ |
||||
import { css } from '@emotion/css'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; |
||||
import { useStyles2 } from '@grafana/ui'; |
||||
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; |
||||
import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; |
||||
|
||||
import { RowItem } from './RowItem'; |
||||
|
||||
interface RowsLayoutManagerState extends SceneObjectState { |
||||
rows: RowItem[]; |
||||
} |
||||
|
||||
export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> implements DashboardLayoutManager { |
||||
public isDashboardLayoutManager: true = true; |
||||
|
||||
public editModeChanged(isEditing: boolean): void {} |
||||
|
||||
public addPanel(vizPanel: VizPanel): void {} |
||||
|
||||
public addNewRow(): void { |
||||
this.setState({ |
||||
rows: [ |
||||
...this.state.rows, |
||||
new RowItem({ |
||||
title: 'New row', |
||||
layout: ResponsiveGridLayoutManager.createEmpty(), |
||||
}), |
||||
], |
||||
}); |
||||
} |
||||
|
||||
public getNextPanelId(): number { |
||||
return 0; |
||||
} |
||||
|
||||
public removePanel(panel: VizPanel) {} |
||||
|
||||
public removeRow(row: RowItem) { |
||||
this.setState({ |
||||
rows: this.state.rows.filter((r) => r !== row), |
||||
}); |
||||
} |
||||
|
||||
public duplicatePanel(panel: VizPanel): void { |
||||
throw new Error('Method not implemented.'); |
||||
} |
||||
|
||||
public getVizPanels(): VizPanel[] { |
||||
const panels: VizPanel[] = []; |
||||
|
||||
for (const row of this.state.rows) { |
||||
const innerPanels = row.state.layout.getVizPanels(); |
||||
panels.push(...innerPanels); |
||||
} |
||||
|
||||
return panels; |
||||
} |
||||
|
||||
public getOptions() { |
||||
return []; |
||||
} |
||||
|
||||
public getDescriptor(): LayoutRegistryItem { |
||||
return RowsLayoutManager.getDescriptor(); |
||||
} |
||||
|
||||
public static getDescriptor(): LayoutRegistryItem { |
||||
return { |
||||
name: 'Rows', |
||||
description: 'Rows layout', |
||||
id: 'rows-layout', |
||||
createFromLayout: RowsLayoutManager.createFromLayout, |
||||
}; |
||||
} |
||||
|
||||
public static createEmpty() { |
||||
return new RowsLayoutManager({ rows: [] }); |
||||
} |
||||
|
||||
public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager { |
||||
const row = new RowItem({ layout: layout.clone(), title: 'Row title' }); |
||||
|
||||
return new RowsLayoutManager({ rows: [row] }); |
||||
} |
||||
|
||||
public static Component = ({ model }: SceneComponentProps<RowsLayoutManager>) => { |
||||
const { rows } = model.useState(); |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
return ( |
||||
<div className={styles.wrapper}> |
||||
{rows.map((row) => ( |
||||
<RowItem.Component model={row} key={row.state.key!} /> |
||||
))} |
||||
</div> |
||||
); |
||||
}; |
||||
} |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
wrapper: css({ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
gap: theme.spacing(1), |
||||
height: '100%', |
||||
width: '100%', |
||||
}), |
||||
}; |
||||
} |
@ -0,0 +1,66 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { Select } from '@grafana/ui'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
||||
|
||||
import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types'; |
||||
|
||||
import { layoutRegistry } from './layoutRegistry'; |
||||
|
||||
export interface Props { |
||||
layoutManager: DashboardLayoutManager; |
||||
} |
||||
|
||||
export function DashboardLayoutSelector({ layoutManager }: { layoutManager: DashboardLayoutManager }) { |
||||
const layouts = layoutRegistry.list(); |
||||
const options = layouts.map((layout) => ({ |
||||
label: layout.name, |
||||
value: layout, |
||||
})); |
||||
|
||||
const currentLayoutId = layoutManager.getDescriptor().id; |
||||
const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId); |
||||
|
||||
return ( |
||||
<Select |
||||
options={options} |
||||
value={currentLayoutOption} |
||||
onChange={(option) => changeLayoutTo(layoutManager, option.value!)} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
export function useLayoutCategory(layoutManager: DashboardLayoutManager) { |
||||
return useMemo(() => { |
||||
const layoutCategory = new OptionsPaneCategoryDescriptor({ |
||||
title: 'Layout', |
||||
id: 'layout-options', |
||||
isOpenDefault: true, |
||||
}); |
||||
|
||||
layoutCategory.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: 'Type', |
||||
render: function renderTitle() { |
||||
return <DashboardLayoutSelector layoutManager={layoutManager} />; |
||||
}, |
||||
}) |
||||
); |
||||
|
||||
if (layoutManager.getOptions) { |
||||
for (const option of layoutManager.getOptions()) { |
||||
layoutCategory.addItem(option); |
||||
} |
||||
} |
||||
|
||||
return layoutCategory; |
||||
}, [layoutManager]); |
||||
} |
||||
|
||||
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) { |
||||
const layoutParent = currentLayout.parent; |
||||
if (layoutParent && isLayoutParent(layoutParent)) { |
||||
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout)); |
||||
} |
||||
} |
@ -1,105 +0,0 @@ |
||||
import { css } from '@emotion/css'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { useStyles2, Field, Select } from '@grafana/ui'; |
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils'; |
||||
import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types'; |
||||
|
||||
import { layoutRegistry } from './layoutRegistry'; |
||||
|
||||
interface Props { |
||||
layoutManager: DashboardLayoutManager; |
||||
children: React.ReactNode; |
||||
} |
||||
|
||||
export function LayoutEditChrome({ layoutManager, children }: Props) { |
||||
const styles = useStyles2(getStyles); |
||||
const { isEditing } = getDashboardSceneFor(layoutManager).useState(); |
||||
|
||||
const layouts = layoutRegistry.list(); |
||||
const options = layouts.map((layout) => ({ |
||||
label: layout.name, |
||||
value: layout, |
||||
})); |
||||
|
||||
const currentLayoutId = layoutManager.getDescriptor().id; |
||||
const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId); |
||||
|
||||
return ( |
||||
<div className={styles.wrapper}> |
||||
{isEditing && ( |
||||
<div className={styles.editHeader}> |
||||
<Field label="Layout type"> |
||||
<Select |
||||
options={options} |
||||
value={currentLayoutOption} |
||||
onChange={(option) => changeLayoutTo(layoutManager, option.value!)} |
||||
/> |
||||
</Field> |
||||
{layoutManager.renderEditor?.()} |
||||
</div> |
||||
)} |
||||
{children} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
editHeader: css({ |
||||
width: '100%', |
||||
display: 'flex', |
||||
gap: theme.spacing(1), |
||||
padding: theme.spacing(0, 1, 0.5, 1), |
||||
margin: theme.spacing(0, 0, 1, 0), |
||||
alignItems: 'flex-end', |
||||
borderBottom: `1px solid ${theme.colors.border.weak}`, |
||||
paddingBottom: theme.spacing(1), |
||||
|
||||
'&:hover, &:focus-within': { |
||||
'& > div': { |
||||
opacity: 1, |
||||
}, |
||||
}, |
||||
|
||||
'& > div': { |
||||
marginBottom: 0, |
||||
marginRight: theme.spacing(1), |
||||
}, |
||||
}), |
||||
wrapper: css({ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
flex: '1 1 0', |
||||
width: '100%', |
||||
}), |
||||
icon: css({ |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
cursor: 'pointer', |
||||
background: 'transparent', |
||||
border: 'none', |
||||
gap: theme.spacing(1), |
||||
}), |
||||
rowTitle: css({}), |
||||
rowActions: css({ |
||||
display: 'flex', |
||||
opacity: 0, |
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: { |
||||
transition: 'opacity 200ms ease-in', |
||||
}, |
||||
|
||||
'&:hover, &:focus-within': { |
||||
opacity: 1, |
||||
}, |
||||
}), |
||||
}; |
||||
} |
||||
|
||||
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) { |
||||
const layoutParent = currentLayout.parent; |
||||
if (layoutParent && isLayoutParent(layoutParent)) { |
||||
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout)); |
||||
} |
||||
} |
Loading…
Reference in new issue