Dashboard: Edit pane in edit mode (#96971)

* Dashboard: Edit pane foundations

* Update

* fix panel edit padding

* Restore scroll pos works when feature toggle is disabled

* Update

* Update

* remember collapsed state

* Update

* fixed padding issue
This commit is contained in:
Torkel Ödegaard 2024-11-26 14:39:09 +01:00 committed by GitHub
parent d2fab92d8b
commit 06d0d41183
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 489 additions and 114 deletions

View File

@ -0,0 +1,84 @@
import { css } from '@emotion/css';
import { useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObjectState, SceneObjectBase, SceneObject, SceneObjectRef } from '@grafana/scenes';
import { ToolbarButton, useStyles2 } from '@grafana/ui';
import { getDashboardSceneFor } from '../utils/utils';
import { ElementEditPane } from './ElementEditPane';
export interface DashboardEditPaneState extends SceneObjectState {
selectedObject?: SceneObjectRef<SceneObject>;
}
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {}
export interface Props {
editPane: DashboardEditPane;
isCollapsed: boolean;
onToggleCollapse: () => void;
}
/**
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
*/
export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse }: Props) {
// Activate the edit pane
useEffect(() => {
if (!editPane.state.selectedObject) {
const dashboard = getDashboardSceneFor(editPane);
editPane.setState({ selectedObject: dashboard.getRef() });
}
editPane.activate();
}, [editPane]);
const { selectedObject } = editPane.useState();
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
if (!selectedObject) {
return null;
}
if (isCollapsed) {
return (
<div className={styles.expandOptionsWrapper}>
<ToolbarButton
tooltip={'Open options pane'}
icon={'arrow-to-right'}
onClick={onToggleCollapse}
variant="canvas"
className={styles.rotate180}
aria-label={'Open options pane'}
/>
</div>
);
}
return (
<div className={styles.wrapper} ref={paneRef}>
<ElementEditPane obj={selectedObject.resolve()} />
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: '1 1 0',
overflow: 'auto',
}),
rotate180: css({
rotate: '180deg',
}),
expandOptionsWrapper: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2, 1),
}),
};
}

View File

@ -0,0 +1,154 @@
import { css, cx } from '@emotion/css';
import React, { CSSProperties, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, useChromeHeaderHeight } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import NativeScrollbar from 'app/core/components/NativeScrollbar';
import { useSnappingSplitter } from '../panel-edit/splitter/useSnappingSplitter';
import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { DashboardEditPaneRenderer } from './DashboardEditPane';
import { useEditPaneCollapsed } from './shared';
interface Props {
dashboard: DashboardScene;
isEditing?: boolean;
body?: React.ReactNode;
controls?: React.ReactNode;
}
export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls }: Props) {
const headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight ?? 0);
const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed();
if (!config.featureToggles.dashboardNewLayouts) {
return (
<NativeScrollbar onSetScrollRef={dashboard.onSetScrollRef}>
<div className={styles.canvasWrappperOld}>
<NavToolbarActions dashboard={dashboard} />
<div className={styles.controlsWrapperSticky}>{controls}</div>
<div className={styles.body}>{body}</div>
</div>
</NativeScrollbar>
);
}
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
useSnappingSplitter({
direction: 'row',
dragPosition: 'end',
initialSize: 0.8,
handleSize: 'sm',
collapsed: isCollapsed,
paneOptions: {
collapseBelowPixels: 250,
snapOpenToPixels: 400,
},
});
useEffect(() => {
setIsCollapsed(splitterState.collapsed);
}, [splitterState.collapsed, setIsCollapsed]);
const containerStyle: CSSProperties = {};
if (!isEditing) {
primaryProps.style.flexGrow = 1;
primaryProps.style.width = '100%';
primaryProps.style.minWidth = 'unset';
containerStyle.overflow = 'unset';
}
const onBodyRef = (ref: HTMLDivElement) => {
dashboard.onSetScrollRef(ref);
};
return (
<div {...containerProps} style={containerStyle}>
<div {...primaryProps} className={cx(primaryProps.className, styles.canvasWithSplitter)}>
<NavToolbarActions dashboard={dashboard} />
<div className={cx(!isEditing && styles.controlsWrapperSticky)}>{controls}</div>
<div className={styles.bodyWrapper}>
<div className={cx(styles.body, isEditing && styles.bodyEditing)} ref={onBodyRef}>
{body}
</div>
</div>
</div>
{isEditing && (
<>
<div {...splitterProps} data-edit-pane-splitter={true} />
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
<DashboardEditPaneRenderer
editPane={dashboard.state.editPane}
isCollapsed={splitterState.collapsed}
onToggleCollapse={onToggleCollapse}
/>
</div>
</>
)}
</div>
);
}
function getStyles(theme: GrafanaTheme2, headerHeight: number) {
return {
canvasWrappperOld: css({
label: 'canvas-wrapper-old',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}),
canvasWithSplitter: css({
overflow: 'unset',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}),
canvasWithSplitterEditing: css({
overflow: 'unset',
}),
bodyWrapper: css({
label: 'body-wrapper',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
position: 'relative',
}),
body: css({
label: 'body',
display: 'flex',
flexGrow: 1,
gap: '8px',
boxSizing: 'border-box',
flexDirection: 'column',
padding: theme.spacing(0, 2, 2, 2),
}),
bodyEditing: css({
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
overflow: 'auto',
scrollbarWidth: 'thin',
}),
editPane: css({
flexDirection: 'column',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
}),
controlsWrapperSticky: css({
[theme.breakpoints.up('md')]: {
position: 'sticky',
zIndex: theme.zIndex.activePanel,
background: theme.colors.background.canvas,
top: headerHeight,
},
}),
};
}

View File

@ -0,0 +1,60 @@
import { useMemo } from 'react';
import { Input, TextArea } from '@grafana/ui';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { DashboardScene } from '../scene/DashboardScene';
import { EditableDashboardElement } from '../scene/types';
export class DummySelectedObject implements EditableDashboardElement {
public isEditableDashboardElement: true = true;
constructor(private dashboard: DashboardScene) {}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const dashboard = this.dashboard;
const dashboardOptions = useMemo(() => {
return new OptionsPaneCategoryDescriptor({
title: 'Dashboard options',
id: 'dashboard-options',
isOpenDefault: true,
})
.addItem(
new OptionsPaneItemDescriptor({
title: 'Title',
render: function renderTitle() {
return <DashboardTitleInput dashboard={dashboard} />;
},
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Description',
render: function renderTitle() {
return <DashboardDescriptionInput dashboard={dashboard} />;
},
})
);
}, [dashboard]);
return [dashboardOptions];
}
public getTypeName(): string {
return 'Dashboard';
}
}
export function DashboardTitleInput({ dashboard }: { dashboard: DashboardScene }) {
const { title } = dashboard.useState();
return <Input value={title} onChange={(e) => dashboard.setState({ title: e.currentTarget.value })} />;
}
export function DashboardDescriptionInput({ dashboard }: { dashboard: DashboardScene }) {
const { description } = dashboard.useState();
return <TextArea value={description} onChange={(e) => dashboard.setState({ title: e.currentTarget.value })} />;
}

View File

@ -0,0 +1,70 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObject } from '@grafana/scenes';
import { Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
import { DashboardScene } from '../scene/DashboardScene';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types';
import { DummySelectedObject } from './DummySelectedObject';
export interface Props {
obj: SceneObject;
}
export function ElementEditPane({ obj }: Props) {
const element = getEditableElementFor(obj);
const categories = element.useEditPaneOptions();
const styles = useStyles2(getStyles);
return (
<Stack direction="column" gap={0}>
{element.renderActions && (
<OptionsPaneCategory
id="selected-item"
title={element.getTypeName()}
isOpenDefault={true}
className={styles.noBorderTop}
>
<div className={styles.actionsBox}>{element.renderActions()}</div>
</OptionsPaneCategory>
)}
{categories.map((cat) => cat.render())}
</Stack>
);
}
function getEditableElementFor(obj: SceneObject): EditableDashboardElement {
if (isEditableDashboardElement(obj)) {
return obj;
}
for (const behavior of obj.state.$behaviors ?? []) {
if (isEditableDashboardElement(behavior)) {
return behavior;
}
}
// Temp thing to show somethin in edit pane
if (obj instanceof DashboardScene) {
return new DummySelectedObject(obj);
}
throw new Error("Can't find editable element for selected object");
}
function getStyles(theme: GrafanaTheme2) {
return {
noBorderTop: css({
borderTop: 'none',
}),
actionsBox: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
paddingBottom: theme.spacing(1),
}),
};
}

View File

@ -0,0 +1,5 @@
import { useSessionStorage } from 'react-use';
export function useEditPaneCollapsed() {
return useSessionStorage('grafana.dashboards.edit-pane.isCollapsed', false);
}

View File

@ -1,10 +1,12 @@
import { css, cx } from '@emotion/css';
import { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
import { useEditPaneCollapsed } from '../edit-pane/shared';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { UnlinkModal } from '../scene/UnlinkModal';
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
@ -17,18 +19,24 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
const dashboard = getDashboardSceneFor(model);
const { optionsPane } = model.useState();
const styles = useStyles2(getStyles);
const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed();
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
useSnappingSplitter({
direction: 'row',
dragPosition: 'end',
initialSize: 0.75,
initialSize: 0.8,
collapsed: isCollapsed,
paneOptions: {
collapseBelowPixels: 250,
snapOpenToPixels: 400,
},
});
useEffect(() => {
setIsCollapsed(splitterState.collapsed);
}, [splitterState.collapsed, setIsCollapsed]);
return (
<>
<NavToolbarActions dashboard={dashboard} />
@ -223,7 +231,6 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
flexGrow: 0,
gridArea: 'controls',
padding: theme.spacing(2, 0, 2, 2),
}),
openDataPaneButton: css({
width: theme.spacing(8),

View File

@ -1,6 +1,6 @@
import { useState, useCallback } from 'react';
import { DragHandlePosition, useSplitter } from '@grafana/ui';
import { ComponentSize, DragHandlePosition, useSplitter } from '@grafana/ui';
export interface UseSnappingSplitterOptions {
/**
@ -10,6 +10,10 @@ export interface UseSnappingSplitterOptions {
direction: 'row' | 'column';
dragPosition?: DragHandlePosition;
paneOptions: PaneOptions;
collapsed?: boolean;
// Size of the region left of the handle indicator that is responsive to dragging. At the same time acts as a margin
// pushing the left pane content left.
handleSize?: ComponentSize;
}
interface PaneOptions {
@ -25,7 +29,10 @@ interface PaneState {
export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
const { paneOptions } = options;
const [state, setState] = useState<PaneState>({ collapsed: false });
const [state, setState] = useState<PaneState>({
collapsed: options.collapsed ?? false,
snapSize: options.collapsed ? 0 : undefined,
});
const onResizing = useCallback(
(flexSize: number, pixelSize: number) => {
@ -76,7 +83,10 @@ export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
}, [state.collapsed]);
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
...options,
direction: options.direction,
dragPosition: options.dragPosition,
handleSize: options.handleSize,
initialSize: options.initialSize,
onResizing,
onSizeChanged,
});

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2, VariableHide } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -127,11 +127,15 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
const showDebugger = location.search.includes('scene-debugger');
if (!model.hasControls()) {
return null;
// To still have spacing when no controls are rendered
return <Box padding={1} />;
}
return (
<div data-testid={selectors.pages.Dashboard.Controls} className={styles.controls}>
<div
data-testid={selectors.pages.Dashboard.Controls}
className={cx(styles.controls, editPanel && styles.controlsPanelEdit)}
>
<Stack grow={1} wrap={'wrap'}>
{!hideVariableControls && variableControls.map((c) => <c.Component model={c} key={c.state.key} />)}
<Box grow={1} />
@ -156,6 +160,7 @@ function getStyles(theme: GrafanaTheme2) {
alignItems: 'flex-start',
flex: '100%',
gap: theme.spacing(1),
padding: theme.spacing(2),
flexDirection: 'row',
flexWrap: 'nowrap',
position: 'relative',
@ -166,6 +171,10 @@ function getStyles(theme: GrafanaTheme2) {
alignItems: 'stretch',
},
}),
controlsPanelEdit: css({
// In panel edit we do not need any right padding as the splitter is providing it
paddingRight: 0,
}),
embedded: css({
background: 'unset',
position: 'unset',

View File

@ -38,6 +38,7 @@ import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
@ -127,6 +128,8 @@ export interface DashboardSceneState extends SceneObjectState {
panelSearch?: string;
/** How many panels to show per row for search results */
panelsPerRow?: number;
/** options pane */
editPane: DashboardEditPane;
}
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
@ -177,6 +180,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
body: state.body ?? DefaultGridLayoutManager.fromVizPanels(),
links: state.links ?? [],
...state,
editPane: new DashboardEditPane({}),
});
this._scopesFacade = getClosestScopesFacade(this);

View File

@ -1,34 +1,38 @@
import { css, cx } from '@emotion/css';
import { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { useChromeHeaderHeight } from '@grafana/runtime';
import { PageLayoutType } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import NativeScrollbar from 'app/core/components/NativeScrollbar';
import { Page } from 'app/core/components/Page/Page';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { getNavModel } from 'app/core/selectors/navModel';
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { useSelector } from 'app/types';
import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter';
import { DashboardScene } from './DashboardScene';
import { NavToolbarActions } from './NavToolbarActions';
import { PanelSearchLayout } from './PanelSearchLayout';
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } =
model.useState();
const headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight ?? 0);
const {
controls,
overlay,
editview,
editPanel,
isEmpty,
meta,
viewPanelScene,
panelSearch,
panelsPerRow,
isEditing,
} = model.useState();
const location = useLocation();
const navIndex = useSelector((state) => state.navIndex);
const pageNav = model.getPageNav(location, navIndex);
const bodyToRender = model.getBodyToRender();
const navModel = getNavModel(navIndex, 'dashboards/browse');
const hasControls = controls?.hasControls();
const isSettingsOpen = editview !== undefined;
// Remember scroll pos when going into view panel, edit panel or settings
@ -54,108 +58,38 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
);
}
const emptyState = (
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
);
function renderBody() {
if (meta.dashboardNotFound) {
return <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
}
const withPanels = (
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)} key="dashboard-panels">
<bodyToRender.Component model={bodyToRender} />
</div>
);
if (panelSearch || panelsPerRow) {
return <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
}
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
const angularBanner = <DashboardAngularDeprecationBanner dashboard={model} key="angular-deprecation-banner" />;
let body: React.ReactNode = [angularBanner, withPanels];
if (notFound) {
body = [notFound];
} else if (isEmpty) {
body = [emptyState, withPanels];
} else if (panelSearch || panelsPerRow) {
body = <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
return (
<>
<DashboardAngularDeprecationBanner dashboard={model} key="angular-deprecation-banner" />
{isEmpty && (
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
)}
<bodyToRender.Component model={bodyToRender} />
</>
);
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
{editPanel && <editPanel.Component model={editPanel} />}
{!editPanel && (
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
<NavToolbarActions dashboard={model} />
{controls && (
<div className={styles.controlsWrapper}>
<controls.Component model={controls} />
</div>
)}
<div className={cx(styles.canvasContent)}>{body}</div>
</div>
</NativeScrollbar>
<DashboardEditPaneSplitter
dashboard={model}
isEditing={isEditing}
controls={controls && <controls.Component model={controls} />}
body={renderBody()}
/>
)}
{overlay && <overlay.Component model={overlay} />}
</Page>
);
}
function getStyles(theme: GrafanaTheme2, headerHeight: number) {
return {
pageContainer: css({
display: 'grid',
gridTemplateAreas: `
"panels"`,
gridTemplateColumns: `1fr`,
gridTemplateRows: '1fr',
flexGrow: 1,
[theme.breakpoints.down('sm')]: {
display: 'flex',
flexDirection: 'column',
},
}),
pageContainerWithControls: css({
gridTemplateAreas: `
"controls"
"panels"`,
gridTemplateRows: 'auto 1fr',
}),
controlsWrapper: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 0,
gridArea: 'controls',
padding: theme.spacing(2),
':empty': {
display: 'none',
},
// Make controls sticky on larger screens (> mobile)
[theme.breakpoints.up('md')]: {
position: 'sticky',
zIndex: theme.zIndex.activePanel,
background: theme.colors.background.canvas,
top: headerHeight,
},
}),
canvasContent: css({
label: 'canvas-content',
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(0.5, 2),
flexBasis: '100%',
gridArea: 'panels',
flexGrow: 1,
minWidth: 0,
}),
body: css({
label: 'body',
flexGrow: 1,
display: 'flex',
gap: '8px',
paddingBottom: theme.spacing(2),
boxSizing: 'border-box',
}),
bodyWithoutControls: css({
paddingTop: theme.spacing(2),
}),
};
}

View File

@ -41,8 +41,19 @@ export class DefaultGridLayoutManager
implements DashboardLayoutManager
{
public editModeChanged(isEditing: boolean): void {
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
forceRenderChildren(this.state.grid, true);
const updateResizeAndDragging = () => {
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
forceRenderChildren(this.state.grid, true);
};
if (config.featureToggles.dashboardNewLayouts) {
// We do this in a timeout to wait a bit with enabling dragging as dragging enables grid animations
// if we show the edit pane without animations it opens much faster and feels more responsive
setTimeout(updateResizeAndDragging, 10);
return;
}
updateResizeAndDragging();
}
public addPanel(vizPanel: VizPanel): void {

View File

@ -73,7 +73,6 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
flex: '1 1 0',
width: '100%',
minHeight: 0,
}),
icon: css({
display: 'flex',

View File

@ -122,3 +122,29 @@ export interface DashboardRepeatsProcessedEventPayload {
export class DashboardRepeatsProcessedEvent extends BusEventWithPayload<DashboardRepeatsProcessedEventPayload> {
public static type = 'dashboard-repeats-processed';
}
/**
* Interface for elements that have options
*/
export interface EditableDashboardElement {
/**
* Marks this object as an element that can be selected and edited directly on the canvas
*/
isEditableDashboardElement: true;
/**
* Hook that returns edit pane optionsß
*/
useEditPaneOptions(): OptionsPaneCategoryDescriptor[];
/**
* Get the type name of the element
*/
getTypeName(): string;
/**
* Panel Actions
**/
renderActions?(): React.ReactNode;
}
export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement {
return 'isEditableDashboardElement' in obj;
}

View File

@ -23,6 +23,7 @@ import {
VariableSort as VariableSortV1,
} from '@grafana/schema/dist/esm/index.gen';
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
@ -132,6 +133,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
}),
}),
meta: {},
editPane: new DashboardEditPane({}),
$behaviors: [
new behaviors.CursorSync({
sync: DashboardCursorSyncV1.Crosshair,