diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e52f47835f5..fc76cf7eabd 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -328,4 +328,5 @@ export { ElementSelectionContext, useElementSelection, type ElementSelectionContextState, + type ElementSelectionContextItem, } from './ElementSelectionContext/ElementSelectionContext'; diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 745782546c7..55ab34be79d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -2,27 +2,78 @@ 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 { + SceneObjectState, + SceneObjectBase, + SceneObject, + SceneObjectRef, + sceneGraph, + useSceneObjectState, +} from '@grafana/scenes'; +import { ElementSelectionContextItem, ElementSelectionContextState, ToolbarButton, useStyles2 } from '@grafana/ui'; -import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types'; import { getDashboardSceneFor } from '../utils/utils'; import { ElementEditPane } from './ElementEditPane'; +import { useEditableElement } from './useEditableElement'; export interface DashboardEditPaneState extends SceneObjectState { selectedObject?: SceneObjectRef; + selectionContext: ElementSelectionContextState; } export class DashboardEditPane extends SceneObjectBase { - public selectObject(obj: SceneObject) { + public constructor() { + super({ + selectionContext: { + enabled: false, + selected: [], + onSelect: (item, multi) => this.selectElement(item, multi), + }, + }); + } + + public enableSelection() { + // Enable element selection + this.setState({ selectionContext: { ...this.state.selectionContext, enabled: true } }); + } + + public disableSelection() { + this.setState({ selectionContext: { ...this.state.selectionContext, enabled: false } }); + } + + private selectElement(element: ElementSelectionContextItem, multi?: boolean) { + const obj = sceneGraph.findByKey(this, element.id); + if (obj) { + this.selectObject(obj, element.id, multi); + } + } + + public selectObject(obj: SceneObject, id: string, multi?: boolean) { const currentSelection = this.state.selectedObject?.resolve(); if (currentSelection === obj) { - const dashboard = getDashboardSceneFor(this); - this.setState({ selectedObject: dashboard.getRef() }); - } else { - this.setState({ selectedObject: obj.getRef() }); + this.clearSelection(); + return; } + + this.setState({ + selectedObject: obj.getRef(), + selectionContext: { + ...this.state.selectionContext, + selected: [{ id }], + }, + }); + } + + public clearSelection() { + const dashboard = getDashboardSceneFor(this); + this.setState({ + selectedObject: dashboard.getRef(), + selectionContext: { + ...this.state.selectionContext, + selected: [], + }, + }); } } @@ -42,14 +93,20 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla const dashboard = getDashboardSceneFor(editPane); editPane.setState({ selectedObject: dashboard.getRef() }); } - editPane.activate(); + + editPane.enableSelection(); + + return () => { + editPane.disableSelection(); + }; }, [editPane]); - const { selectedObject } = editPane.useState(); + const { selectedObject } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const styles = useStyles2(getStyles); const paneRef = useRef(null); + const editableElement = useEditableElement(selectedObject?.resolve()); - if (!selectedObject) { + if (!editableElement) { return null; } @@ -68,29 +125,13 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla ); } - const element = getEditableElementFor(selectedObject.resolve()); - return (
- +
); } -function getEditableElementFor(obj: SceneObject): EditableDashboardElement { - if (isEditableDashboardElement(obj)) { - return obj; - } - - for (const behavior of obj.state.$behaviors ?? []) { - if (isEditableDashboardElement(behavior)) { - return behavior; - } - } - - throw new Error("Can't find editable element for selected object"); -} - function getStyles(theme: GrafanaTheme2) { return { wrapper: css({ diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index 5e1ce167895..c45c47be262 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -3,7 +3,8 @@ import React, { CSSProperties, useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { config, useChromeHeaderHeight } from '@grafana/runtime'; -import { useStyles2 } from '@grafana/ui'; +import { useSceneObjectState } from '@grafana/scenes'; +import { ElementSelectionContext, useStyles2 } from '@grafana/ui'; import NativeScrollbar from 'app/core/components/NativeScrollbar'; import { useSnappingSplitter } from '../panel-edit/splitter/useSnappingSplitter'; @@ -22,6 +23,7 @@ interface Props { export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls }: Props) { const headerHeight = useChromeHeaderHeight(); + const { editPane } = dashboard.state; const styles = useStyles2(getStyles, headerHeight ?? 0); const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); @@ -55,6 +57,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls setIsCollapsed(splitterState.collapsed); }, [splitterState.collapsed, setIsCollapsed]); + const { selectionContext } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const containerStyle: CSSProperties = {}; if (!isEditing) { @@ -70,12 +73,16 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls return (
-
+
editPane.clearSelection()} + >
{controls}
- {body} + {body}
@@ -84,7 +91,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
@@ -136,6 +143,8 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { bottom: 0, overflow: 'auto', scrollbarWidth: 'thin', + // The fixed controls headers is otherwise rendered over the selection outlinem, Maybe there is an other solution + paddingTop: '2px', }), editPane: css({ flexDirection: 'column', diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx similarity index 89% rename from public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx rename to public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 5e195498da2..f3814c2a7e1 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; -import { SceneObjectBase } from '@grafana/scenes'; import { Input, TextArea } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -8,13 +7,14 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; import { EditableDashboardElement } from '../scene/types'; -import { getDashboardSceneFor } from '../utils/utils'; -export class DashboardEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement { +export class DashboardEditableElement implements EditableDashboardElement { public isEditableDashboardElement: true = true; + public constructor(private dashboard: DashboardScene) {} + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { - const dashboard = getDashboardSceneFor(this); + const dashboard = this.dashboard; // When layout changes we need to update options list const { body } = dashboard.useState(); diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx similarity index 82% rename from public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx rename to public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 16ba425515d..a4b67f412d7 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react'; -import { sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes'; +import { sceneGraph, VizPanel } from '@grafana/scenes'; import { Button } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; @@ -14,21 +15,13 @@ import { import { EditableDashboardElement, isDashboardLayoutItem } from '../scene/types'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; -export class VizPanelEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement { +export class VizPanelEditableElement implements EditableDashboardElement { public isEditableDashboardElement: true = true; - private getPanel(): VizPanel { - const panel = this.parent; - - if (!(panel instanceof VizPanel)) { - throw new Error('VizPanelEditPaneBehavior must have a VizPanel parent'); - } - - return panel; - } + public constructor(private panel: VizPanel) {} public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { - const panel = this.getPanel(); + const panel = this.panel; const layoutElement = panel.parent!; const panelOptions = useMemo(() => { @@ -108,22 +101,18 @@ export class VizPanelEditPaneBehavior extends SceneObjectBase implements Editabl } public onDelete = () => { - const layout = dashboardSceneGraph.getLayoutManagerFor(this); - layout.removePanel(this.getPanel()); + const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel); + layout.removePanel(this.panel); }; public renderActions(): React.ReactNode { return ( <> - - + - - + - {isEditing &&
{!isCollapsed && }
diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 1fa3e29bdf2..a0471add33d 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -1,9 +1,10 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; +import { DashboardScene } from '../DashboardScene'; import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; @@ -18,7 +19,22 @@ export class RowsLayoutManager extends SceneObjectBase i public editModeChanged(isEditing: boolean): void {} - public addPanel(vizPanel: VizPanel): void {} + public addPanel(vizPanel: VizPanel): void { + // Try to add new panels to the selected row + const selectedObject = this.getSelectedObject(); + if (selectedObject instanceof RowItem) { + return selectedObject.onAddPanel(vizPanel); + } + + // If we don't have selected row add it to the first row + if (this.state.rows.length > 0) { + return this.state.rows[0].onAddPanel(vizPanel); + } + + // Otherwise fallback to adding a new row and a panel + this.addNewRow(); + this.state.rows[this.state.rows.length - 1].onAddPanel(vizPanel); + } public addNewRow(): void { this.setState({ @@ -67,6 +83,10 @@ export class RowsLayoutManager extends SceneObjectBase i return RowsLayoutManager.getDescriptor(); } + public getSelectedObject() { + return sceneGraph.getAncestor(this, DashboardScene).state.editPane.state.selectedObject?.resolve(); + } + public static getDescriptor(): LayoutRegistryItem { return { name: 'Rows', @@ -106,7 +126,7 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', flexDirection: 'column', gap: theme.spacing(1), - height: '100%', + flexGrow: 1, width: '100%', }), }; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index cb6b746d137..22a8c119054 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -27,7 +27,6 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; -import { DashboardEditPaneBehavior } from '../edit-pane/DashboardEditPaneBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -224,7 +223,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, new behaviors.SceneQueryController(), registerDashboardMacro, registerPanelInteractionsReporter, - new DashboardEditPaneBehavior({}), new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), preserveDashboardSceneStateInLocalStorage, addPanelsOnLoadBehavior, @@ -238,11 +236,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, version: oldModel.version, }), ]; - - if (config.featureToggles.dashboardNewLayouts) { - behaviorList.push(new DashboardEditPaneBehavior({})); - } - const dashboardScene = new DashboardScene({ description: oldModel.description, editable: oldModel.editable, diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 035c7298a8a..ce5a898e5ed 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -176,7 +176,7 @@ describe('transformSceneToSaveModelSchemaV2', () => { }), }), meta: {}, - editPane: new DashboardEditPane({}), + editPane: new DashboardEditPane(), $behaviors: [ new behaviors.CursorSync({ sync: DashboardCursorSyncV1.Crosshair,