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 { css, cx } from '@emotion/css';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
||||||
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
|
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { useEditPaneCollapsed } from '../edit-pane/shared';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||||
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
||||||
|
@ -17,18 +19,24 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||||
const dashboard = getDashboardSceneFor(model);
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const { optionsPane } = model.useState();
|
const { optionsPane } = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed();
|
||||||
|
|
||||||
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
|
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
|
||||||
useSnappingSplitter({
|
useSnappingSplitter({
|
||||||
direction: 'row',
|
direction: 'row',
|
||||||
dragPosition: 'end',
|
dragPosition: 'end',
|
||||||
initialSize: 0.75,
|
initialSize: 0.8,
|
||||||
|
collapsed: isCollapsed,
|
||||||
paneOptions: {
|
paneOptions: {
|
||||||
collapseBelowPixels: 250,
|
collapseBelowPixels: 250,
|
||||||
snapOpenToPixels: 400,
|
snapOpenToPixels: 400,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsCollapsed(splitterState.collapsed);
|
||||||
|
}, [splitterState.collapsed, setIsCollapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
|
@ -223,7 +231,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
gridArea: 'controls',
|
gridArea: 'controls',
|
||||||
padding: theme.spacing(2, 0, 2, 2),
|
|
||||||
}),
|
}),
|
||||||
openDataPaneButton: css({
|
openDataPaneButton: css({
|
||||||
width: theme.spacing(8),
|
width: theme.spacing(8),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import { DragHandlePosition, useSplitter } from '@grafana/ui';
|
import { ComponentSize, DragHandlePosition, useSplitter } from '@grafana/ui';
|
||||||
|
|
||||||
export interface UseSnappingSplitterOptions {
|
export interface UseSnappingSplitterOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +10,10 @@ export interface UseSnappingSplitterOptions {
|
||||||
direction: 'row' | 'column';
|
direction: 'row' | 'column';
|
||||||
dragPosition?: DragHandlePosition;
|
dragPosition?: DragHandlePosition;
|
||||||
paneOptions: PaneOptions;
|
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 {
|
interface PaneOptions {
|
||||||
|
@ -25,7 +29,10 @@ interface PaneState {
|
||||||
export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
||||||
const { paneOptions } = options;
|
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(
|
const onResizing = useCallback(
|
||||||
(flexSize: number, pixelSize: number) => {
|
(flexSize: number, pixelSize: number) => {
|
||||||
|
@ -76,7 +83,10 @@ export function useSnappingSplitter(options: UseSnappingSplitterOptions) {
|
||||||
}, [state.collapsed]);
|
}, [state.collapsed]);
|
||||||
|
|
||||||
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
|
const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
|
||||||
...options,
|
direction: options.direction,
|
||||||
|
dragPosition: options.dragPosition,
|
||||||
|
handleSize: options.handleSize,
|
||||||
|
initialSize: options.initialSize,
|
||||||
onResizing,
|
onResizing,
|
||||||
onSizeChanged,
|
onSizeChanged,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2, VariableHide } from '@grafana/data';
|
import { GrafanaTheme2, VariableHide } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
@ -127,11 +127,15 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||||
const showDebugger = location.search.includes('scene-debugger');
|
const showDebugger = location.search.includes('scene-debugger');
|
||||||
|
|
||||||
if (!model.hasControls()) {
|
if (!model.hasControls()) {
|
||||||
return null;
|
// To still have spacing when no controls are rendered
|
||||||
|
return <Box padding={1} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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'}>
|
<Stack grow={1} wrap={'wrap'}>
|
||||||
{!hideVariableControls && variableControls.map((c) => <c.Component model={c} key={c.state.key} />)}
|
{!hideVariableControls && variableControls.map((c) => <c.Component model={c} key={c.state.key} />)}
|
||||||
<Box grow={1} />
|
<Box grow={1} />
|
||||||
|
@ -156,6 +160,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
flex: '100%',
|
flex: '100%',
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
|
padding: theme.spacing(2),
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
@ -166,6 +171,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
controlsPanelEdit: css({
|
||||||
|
// In panel edit we do not need any right padding as the splitter is providing it
|
||||||
|
paddingRight: 0,
|
||||||
|
}),
|
||||||
embedded: css({
|
embedded: css({
|
||||||
background: 'unset',
|
background: 'unset',
|
||||||
position: 'unset',
|
position: 'unset',
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { VariablesChanged } from 'app/features/variables/types';
|
||||||
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
|
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
|
|
||||||
|
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
||||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||||
|
@ -127,6 +128,8 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||||
panelSearch?: string;
|
panelSearch?: string;
|
||||||
/** How many panels to show per row for search results */
|
/** How many panels to show per row for search results */
|
||||||
panelsPerRow?: number;
|
panelsPerRow?: number;
|
||||||
|
/** options pane */
|
||||||
|
editPane: DashboardEditPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||||
|
@ -177,6 +180,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||||
body: state.body ?? DefaultGridLayoutManager.fromVizPanels(),
|
body: state.body ?? DefaultGridLayoutManager.fromVizPanels(),
|
||||||
links: state.links ?? [],
|
links: state.links ?? [],
|
||||||
...state,
|
...state,
|
||||||
|
editPane: new DashboardEditPane({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._scopesFacade = getClosestScopesFacade(this);
|
this._scopesFacade = getClosestScopesFacade(this);
|
||||||
|
|
|
@ -1,34 +1,38 @@
|
||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom-v5-compat';
|
import { useLocation } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { PageLayoutType } from '@grafana/data';
|
||||||
import { useChromeHeaderHeight } from '@grafana/runtime';
|
|
||||||
import { SceneComponentProps } from '@grafana/scenes';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
|
import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter';
|
||||||
|
|
||||||
import { DashboardScene } from './DashboardScene';
|
import { DashboardScene } from './DashboardScene';
|
||||||
import { NavToolbarActions } from './NavToolbarActions';
|
|
||||||
import { PanelSearchLayout } from './PanelSearchLayout';
|
import { PanelSearchLayout } from './PanelSearchLayout';
|
||||||
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
|
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
|
||||||
|
|
||||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||||
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } =
|
const {
|
||||||
model.useState();
|
controls,
|
||||||
const headerHeight = useChromeHeaderHeight();
|
overlay,
|
||||||
const styles = useStyles2(getStyles, headerHeight ?? 0);
|
editview,
|
||||||
|
editPanel,
|
||||||
|
isEmpty,
|
||||||
|
meta,
|
||||||
|
viewPanelScene,
|
||||||
|
panelSearch,
|
||||||
|
panelsPerRow,
|
||||||
|
isEditing,
|
||||||
|
} = model.useState();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navIndex = useSelector((state) => state.navIndex);
|
const navIndex = useSelector((state) => state.navIndex);
|
||||||
const pageNav = model.getPageNav(location, navIndex);
|
const pageNav = model.getPageNav(location, navIndex);
|
||||||
const bodyToRender = model.getBodyToRender();
|
const bodyToRender = model.getBodyToRender();
|
||||||
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
||||||
const hasControls = controls?.hasControls();
|
|
||||||
const isSettingsOpen = editview !== undefined;
|
const isSettingsOpen = editview !== undefined;
|
||||||
|
|
||||||
// Remember scroll pos when going into view panel, edit panel or settings
|
// Remember scroll pos when going into view panel, edit panel or settings
|
||||||
|
@ -54,108 +58,38 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyState = (
|
function renderBody() {
|
||||||
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
|
if (meta.dashboardNotFound) {
|
||||||
);
|
return <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
|
||||||
|
}
|
||||||
|
|
||||||
const withPanels = (
|
if (panelSearch || panelsPerRow) {
|
||||||
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)} key="dashboard-panels">
|
return <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
|
||||||
<bodyToRender.Component model={bodyToRender} />
|
}
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
|
return (
|
||||||
|
<>
|
||||||
const angularBanner = <DashboardAngularDeprecationBanner dashboard={model} key="angular-deprecation-banner" />;
|
<DashboardAngularDeprecationBanner dashboard={model} key="angular-deprecation-banner" />
|
||||||
|
{isEmpty && (
|
||||||
let body: React.ReactNode = [angularBanner, withPanels];
|
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
|
||||||
|
)}
|
||||||
if (notFound) {
|
<bodyToRender.Component model={bodyToRender} />
|
||||||
body = [notFound];
|
</>
|
||||||
} else if (isEmpty) {
|
);
|
||||||
body = [emptyState, withPanels];
|
|
||||||
} else if (panelSearch || panelsPerRow) {
|
|
||||||
body = <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||||
{editPanel && <editPanel.Component model={editPanel} />}
|
{editPanel && <editPanel.Component model={editPanel} />}
|
||||||
{!editPanel && (
|
{!editPanel && (
|
||||||
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
|
<DashboardEditPaneSplitter
|
||||||
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
|
dashboard={model}
|
||||||
<NavToolbarActions dashboard={model} />
|
isEditing={isEditing}
|
||||||
{controls && (
|
controls={controls && <controls.Component model={controls} />}
|
||||||
<div className={styles.controlsWrapper}>
|
body={renderBody()}
|
||||||
<controls.Component model={controls} />
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cx(styles.canvasContent)}>{body}</div>
|
|
||||||
</div>
|
|
||||||
</NativeScrollbar>
|
|
||||||
)}
|
)}
|
||||||
{overlay && <overlay.Component model={overlay} />}
|
{overlay && <overlay.Component model={overlay} />}
|
||||||
</Page>
|
</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
|
implements DashboardLayoutManager
|
||||||
{
|
{
|
||||||
public editModeChanged(isEditing: boolean): void {
|
public editModeChanged(isEditing: boolean): void {
|
||||||
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
|
const updateResizeAndDragging = () => {
|
||||||
forceRenderChildren(this.state.grid, true);
|
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 {
|
public addPanel(vizPanel: VizPanel): void {
|
||||||
|
|
|
@ -73,7 +73,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flex: '1 1 0',
|
flex: '1 1 0',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: 0,
|
|
||||||
}),
|
}),
|
||||||
icon: css({
|
icon: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
@ -122,3 +122,29 @@ export interface DashboardRepeatsProcessedEventPayload {
|
||||||
export class DashboardRepeatsProcessedEvent extends BusEventWithPayload<DashboardRepeatsProcessedEventPayload> {
|
export class DashboardRepeatsProcessedEvent extends BusEventWithPayload<DashboardRepeatsProcessedEventPayload> {
|
||||||
public static type = 'dashboard-repeats-processed';
|
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,
|
VariableSort as VariableSortV1,
|
||||||
} from '@grafana/schema/dist/esm/index.gen';
|
} from '@grafana/schema/dist/esm/index.gen';
|
||||||
|
|
||||||
|
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||||
|
@ -132,6 +133,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
meta: {},
|
meta: {},
|
||||||
|
editPane: new DashboardEditPane({}),
|
||||||
$behaviors: [
|
$behaviors: [
|
||||||
new behaviors.CursorSync({
|
new behaviors.CursorSync({
|
||||||
sync: DashboardCursorSyncV1.Crosshair,
|
sync: DashboardCursorSyncV1.Crosshair,
|
||||||
|
|
Loading…
Reference in New Issue