From 5a916db4dead0f3f1994947efbfc997b0c6c0d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Mar 2025 16:48:05 +0100 Subject: [PATCH] Dashboard: Scroll element into view when selecting from outline (#102966) * Dashboard: Scroll element into view when selecting from outline * fix import * Switch to tab when selected via outline * Update --------- Co-authored-by: Oscar Kilhed --- .../edit-pane/DashboardOutline.tsx | 4 +- .../edit-pane/VizPanelEditableElement.tsx | 7 +++ .../ResponsiveGridItem.tsx | 20 ++++----- .../ResponsiveGridItemRenderer.tsx | 2 +- .../scene/layout-rows/RowItem.tsx | 8 ++++ .../scene/layout-rows/RowItemRenderer.tsx | 1 + .../scene/layout-tabs/TabItem.tsx | 13 ++++++ .../scene/layout-tabs/TabItemRenderer.tsx | 1 + .../scene/layout-tabs/TabsLayoutManager.tsx | 4 ++ .../scrollCanvasElementIntoView.ts | 43 +++++++++++++++++++ .../scene/types/EditableDashboardElement.ts | 5 +++ 11 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/scrollCanvasElementIntoView.ts diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 63ad898edb5..55841138a99 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -37,6 +37,7 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO const children = collectEditableElementChildren(sceneObject); const elementInfo = editableElement.getEditableElementInfo(); + const instanceName = elementInfo.instanceName === '' ? '' : elementInfo.instanceName; return ( <> @@ -46,11 +47,12 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO onPointerDown={(evt) => { onSelect?.(evt); setIsExpanded(!isExpanded); + editableElement.scrollIntoView?.(); }} > {expandable && } - {elementInfo.instanceName} + {instanceName} {expandable && isExpanded && ( diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index bc1b9b77e8c..70caad547be 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -12,6 +12,7 @@ import { PanelDescriptionTextArea, PanelFrameTitleInput, } from '../panel-edit/getPanelFrameOptions'; +import { AutoGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; import { BulkActionElement } from '../scene/types/BulkActionElement'; import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; @@ -96,6 +97,12 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc public createMultiSelectedElement(items: VizPanelEditableElement[]) { return new MultiSelectedVizPanelsEditableElement(items); } + + public scrollIntoView() { + if (this.panel.parent instanceof AutoGridItem) { + this.panel.parent.scrollIntoView(); + } + } } type OpenPanelEditVizProps = { diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx index af6e1b78687..e314190b244 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx @@ -1,4 +1,5 @@ import { isEqual } from 'lodash'; +import React from 'react'; import { CustomVariable, @@ -18,6 +19,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; import { getCloneKey } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; +import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; @@ -43,9 +45,7 @@ export class AutoGridItem extends SceneObjectBase implements }); public readonly isDashboardLayoutItem = true; - - public _containerRef: HTMLDivElement | null = null; - + public containerRef = React.createRef(); private _prevRepeatValues?: VariableValueSingle[]; public constructor(state: AutoGridItemState) { @@ -153,18 +153,18 @@ export class AutoGridItem extends SceneObjectBase implements return this.parent; } - public setRef(ref: HTMLDivElement | null) { - this._containerRef = ref; - } - public getBoundingBox(): { width: number; height: number; top: number; left: number } { - const rect = this._containerRef!.getBoundingClientRect(); + const rect = this.containerRef.current!.getBoundingClientRect(); return { width: rect.width, height: rect.height, - top: this._containerRef!.offsetTop, - left: this._containerRef!.offsetLeft, + top: this.containerRef.current!.offsetTop, + left: this.containerRef.current!.offsetLeft, }; } + + public scrollIntoView() { + scrollCanvasElementIntoView(this, this.containerRef); + } } diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx index 3e21119dc45..d1d895dfbf5 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemRenderer.tsx @@ -35,7 +35,7 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps ) : ( -
model.setRef(ref)} data-auto-grid-item-drop-target={isDragging ? key : undefined}> +
{isDragged &&
}
(); public constructor(state?: Partial) { super({ @@ -201,4 +205,8 @@ export class RowItem private _getRepeatBehavior(): RowItemRepeaterBehavior | undefined { return this.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior); } + + public scrollIntoView() { + scrollCanvasElementIntoView(this, this.containerRef); + } } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index d7544d778b6..580c7bd051b 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -42,6 +42,7 @@ export function RowItemRenderer({ model }: SceneComponentProps) { return (
(); constructor(state?: Partial) { super({ @@ -160,4 +164,13 @@ export class TabItem private _getParentLayout(): TabsLayoutManager { return sceneGraph.getAncestor(this, TabsLayoutManager); } + + public scrollIntoView(): void { + const tabsLayout = sceneGraph.getAncestor(this, TabsLayoutManager); + if (tabsLayout.getCurrentTab() !== this) { + tabsLayout.switchToTab(this); + } + + scrollCanvasElementIntoView(this, this.containerRef); + } } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 2b43c7908f7..165ed5284de 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -22,6 +22,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { return ( i } } + public switchToTab(tab: TabItem) { + this.setState({ currentTabIndex: this.state.tabs.indexOf(tab) }); + } + public getCurrentTab(): TabItem { return this.state.tabs.length > this.state.currentTabIndex ? this.state.tabs[this.state.currentTabIndex] diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/scrollCanvasElementIntoView.ts b/public/app/features/dashboard-scene/scene/layouts-shared/scrollCanvasElementIntoView.ts new file mode 100644 index 00000000000..782519df429 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/scrollCanvasElementIntoView.ts @@ -0,0 +1,43 @@ +import { SceneObject } from '@grafana/scenes'; + +import { RowItem } from '../layout-rows/RowItem'; +import { TabItem } from '../layout-tabs/TabItem'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; + +/** + * Will scroll element into view. If element is not connected yet, it will try to expand rows + * and switch tabs to make it visible. + */ +export function scrollCanvasElementIntoView(sceneObject: SceneObject, ref: React.RefObject) { + if (ref.current?.isConnected) { + scrollIntoView(ref.current); + return; + } + + // try expanding rows and switching tabs + let parent = sceneObject.parent; + while (parent) { + if (parent instanceof RowItem && parent.state.collapse) { + parent.onCollapseToggle(); + } + + if (parent instanceof TabItem) { + const tabsManager = parent.parent; + if (tabsManager instanceof TabsLayoutManager && tabsManager.getCurrentTab() !== parent) { + tabsManager.switchToTab(parent); + } + } + parent = parent.parent; + } + + // now try to scroll into view + setTimeout(() => { + if (ref.current?.isConnected) { + scrollIntoView(ref.current); + } + }, 10); +} + +function scrollIntoView(element: HTMLElement) { + element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); +} diff --git a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts index 6139b51e8a6..15ec993d1be 100644 --- a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts +++ b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts @@ -44,6 +44,11 @@ export interface EditableDashboardElement { * creates a new multi-selection element from a list of selected items */ createMultiSelectedElement?(elements: this[]): EditableDashboardElement; + + /** + * scroll element into view (when selected from outline) + */ + scrollIntoView?(): void; } export interface EditableDashboardElementInfo {