From 06d0d41183d3dba1c348095b97911f95487ad991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 26 Nov 2024 14:39:09 +0100 Subject: [PATCH] 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 --- .../edit-pane/DashboardEditPane.tsx | 84 ++++++++++ .../edit-pane/DashboardEditPaneSplitter.tsx | 154 ++++++++++++++++++ .../edit-pane/DummySelectedObject.tsx | 60 +++++++ .../edit-pane/ElementEditPane.tsx | 70 ++++++++ .../dashboard-scene/edit-pane/shared.ts | 5 + .../panel-edit/PanelEditorRenderer.tsx | 11 +- .../splitter/useSnappingSplitter.ts | 16 +- .../scene/DashboardControls.tsx | 15 +- .../dashboard-scene/scene/DashboardScene.tsx | 4 + .../scene/DashboardSceneRenderer.tsx | 140 +++++----------- .../DefaultGridLayoutManager.tsx | 15 +- .../scene/layouts-shared/LayoutEditChrome.tsx | 1 - .../features/dashboard-scene/scene/types.ts | 26 +++ .../transformSceneToSaveModelSchemaV2.test.ts | 2 + 14 files changed, 489 insertions(+), 114 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx create mode 100644 public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx create mode 100644 public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx create mode 100644 public/app/features/dashboard-scene/edit-pane/shared.ts diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx new file mode 100644 index 00000000000..b0959584c68 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -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; +} + +export class DashboardEditPane extends SceneObjectBase {} + +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(null); + + if (!selectedObject) { + return null; + } + + if (isCollapsed) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +} + +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), + }), + }; +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx new file mode 100644 index 00000000000..5e1ce167895 --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -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 ( + +
+ +
{controls}
+
{body}
+
+
+ ); + } + + 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 ( +
+
+ +
{controls}
+
+
+ {body} +
+
+
+ {isEditing && ( + <> +
+
+ +
+ + )} +
+ ); +} + +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, + }, + }), + }; +} diff --git a/public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx b/public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx new file mode 100644 index 00000000000..0be2f6511af --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx @@ -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 ; + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Description', + render: function renderTitle() { + return ; + }, + }) + ); + }, [dashboard]); + + return [dashboardOptions]; + } + + public getTypeName(): string { + return 'Dashboard'; + } +} + +export function DashboardTitleInput({ dashboard }: { dashboard: DashboardScene }) { + const { title } = dashboard.useState(); + + return dashboard.setState({ title: e.currentTarget.value })} />; +} + +export function DashboardDescriptionInput({ dashboard }: { dashboard: DashboardScene }) { + const { description } = dashboard.useState(); + + return