mirror of https://github.com/grafana/grafana.git
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:
parent
d2fab92d8b
commit
06d0d41183
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -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 })} />;
|
||||
}
|
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { useSessionStorage } from 'react-use';
|
||||
|
||||
export function useEditPaneCollapsed() {
|
||||
return useSessionStorage('grafana.dashboards.edit-pane.isCollapsed', false);
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -73,7 +73,6 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
width: '100%',
|
||||
minHeight: 0,
|
||||
}),
|
||||
icon: css({
|
||||
display: 'flex',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue