From da5209be1e74e13a5984e88e00c8020bac2e3540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 25 Aug 2025 12:23:11 +0200 Subject: [PATCH] Dashboards: Add support for full screen panel view and embedded (solo panel) route to repeated panels and new layouts (via new SoloPanelContex) (#107375) * SoloPanelContext: First steps * working in default grid * Fixes * Update * SoloPanelPage * Fixing search layout * Fixes * Fixes * Update * remove comments * Solving panel not found * Fix * fix lint * fix lint --- .../pages/PublicDashboardScenePage.tsx | 5 +- .../dashboard-scene/scene/DashboardScene.tsx | 24 ++- .../scene/DashboardSceneRenderer.tsx | 26 +++- .../scene/DashboardSceneUrlSync.test.ts | 57 +------ .../scene/DashboardSceneUrlSync.ts | 56 +------ .../scene/NavToolbarActions.tsx | 4 +- .../scene/PanelMenuBehavior.tsx | 8 +- .../scene/PanelSearchLayout.tsx | 107 +++---------- .../scene/SoloPanelContext.tsx | 141 ++++++++++++++++++ .../scene/keyboardShortcuts.ts | 13 +- .../scene/layout-auto-grid/AutoGridItem.tsx | 3 - .../layout-auto-grid/AutoGridItemRenderer.tsx | 7 +- .../AutoGridLayoutRenderer.tsx | 6 + .../layout-default/DashboardGridItem.tsx | 3 - .../DashboardGridItemRenderer.tsx | 7 + .../DefaultGridLayoutManager.tsx | 20 +++ .../layout-default/RowRepeaterBehavior.ts | 4 - .../scene/layout-rows/RowItemRenderer.tsx | 6 + .../scene/layout-rows/RowItemRepeater.tsx | 2 - .../layout-rows/RowsLayoutManagerRenderer.tsx | 6 + .../scene/layout-tabs/TabItemRepeater.tsx | 2 - .../layout-tabs/TabsLayoutManagerRenderer.tsx | 6 + .../scene/new-toolbar/LeftActions.tsx | 4 +- .../scene/new-toolbar/RightActions.tsx | 4 +- .../types/DashboardRepeatsProcessedEvent.ts | 10 -- .../dashboard-scene/solo/SoloPanelPage.tsx | 35 ++--- .../solo/useSoloPanel.test.tsx | 133 ----------------- .../dashboard-scene/solo/useSoloPanel.ts | 70 --------- public/locales/en-US/grafana.json | 13 +- 29 files changed, 298 insertions(+), 484 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/SoloPanelContext.tsx delete mode 100644 public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts delete mode 100644 public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx delete mode 100644 public/app/features/dashboard-scene/solo/useSoloPanel.ts diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx index 07bcf478336..bac31474da6 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -70,9 +70,8 @@ export function PublicDashboardScenePage({ route }: Props) { function PublicDashboardSceneRenderer({ model }: SceneComponentProps) { const [isActive, setIsActive] = useState(false); - const { controls, title } = model.useState(); + const { controls, title, body } = model.useState(); const { timePicker, refreshPicker, hideTimeControls } = controls!.useState(); - const bodyToRender = model.getBodyToRender(); const styles = useStyles2(getStyles); const conf = useGetPublicDashboardConfig(); @@ -108,7 +107,7 @@ function PublicDashboardSceneRenderer({ model }: SceneComponentProps
- +
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 4d85840b1d4..a8a863a8c4c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -65,7 +65,6 @@ import { isRepeatCloneOrChildOf } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/getDashboardUrl'; -import { getViewPanelUrl } from '../utils/urlBuilders'; import { getClosestVizPanel, getDashboardSceneFor, @@ -81,7 +80,6 @@ import { DashboardLayoutOrchestrator } from './DashboardLayoutOrchestrator'; import { DashboardSceneRenderer } from './DashboardSceneRenderer'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; -import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; import { AutoGridItem } from './layout-auto-grid/AutoGridItem'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; @@ -129,8 +127,10 @@ export interface DashboardSceneState extends SceneObjectState { meta: Omit; /** Version of the dashboard */ version?: number; - /** Panel to view in fullscreen */ - viewPanelScene?: ViewPanelScene; + /** Panel to inspect */ + inspectPanelKey?: string; + /** Panel key to view in fullscreen */ + viewPanel?: string; /** Edit view */ editview?: DashboardEditView; /** Edit panel */ @@ -427,7 +427,7 @@ export class DashboardScene extends SceneObjectBase impleme } public getPageNav(location: H.Location, navIndex: NavIndex) { - const { meta, viewPanelScene, editPanel, title, uid } = this.state; + const { meta, viewPanel, editPanel, title, uid } = this.state; const isNew = !Boolean(uid); let pageNav: NavModelItem = { @@ -456,11 +456,14 @@ export class DashboardScene extends SceneObjectBase impleme } } - if (viewPanelScene) { + if (viewPanel) { pageNav = { text: t('dashboard-scene.dashboard-scene.text.view-panel', 'View panel'), parentItem: pageNav, - url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()), + url: locationUtil.getUrlForPartial(locationService.getLocation(), { + viewPanel: viewPanel, + editPanel: undefined, + }), }; } @@ -474,13 +477,6 @@ export class DashboardScene extends SceneObjectBase impleme return pageNav; } - /** - * Returns the body (layout) or the full view panel - */ - public getBodyToRender(): SceneObject { - return this.state.viewPanelScene ?? this.state.body; - } - public getInitialState(): DashboardSceneState | undefined { return this._initialState; } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 19a6916552c..c0c425da67a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -12,14 +12,16 @@ import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitte import { DashboardScene } from './DashboardScene'; import { PanelSearchLayout } from './PanelSearchLayout'; +import { SoloPanelContextProvider, useDefineSoloPanelContext } from './SoloPanelContext'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { const { controls, overlay, editview, + body, editPanel, - viewPanelScene, + viewPanel, panelSearch, panelsPerRow, isEditing, @@ -30,23 +32,23 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps state.navIndex); const pageNav = model.getPageNav(location, navIndex); - const bodyToRender = model.getBodyToRender(); const navModel = getNavModel(navIndex, `dashboards/${type === 'snapshot' ? 'snapshots' : 'browse'}`); const isSettingsOpen = editview !== undefined; + const soloPanelContext = useDefineSoloPanelContext(viewPanel); // Remember scroll pos when going into view panel, edit panel or settings useMemo(() => { - if (viewPanelScene || isSettingsOpen || editPanel) { + if (viewPanel || isSettingsOpen || editPanel) { model.rememberScrollPos(); } - }, [isSettingsOpen, editPanel, viewPanelScene, model]); + }, [isSettingsOpen, editPanel, viewPanel, model]); // Restore scroll pos when coming back useEffect(() => { - if (!viewPanelScene && !isSettingsOpen && !editPanel) { + if (!viewPanel && !isSettingsOpen && !editPanel) { model.restoreScrollPos(); } - }, [isSettingsOpen, editPanel, viewPanelScene, model]); + }, [isSettingsOpen, editPanel, viewPanel, model]); useEffect(() => { if (scopesContext && isEditing) { @@ -70,11 +72,19 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; } - return ; + if (soloPanelContext) { + return ( + + + + ); + } + + return ; } return ( diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index d89a729ca17..88ad6853dc1 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,21 +1,11 @@ -import { AppEvents } from '@grafana/data'; -import { LocalValueVariable, SceneQueryRunner, SceneVariableSet, VizPanel } from '@grafana/scenes'; -import appEvents from 'app/core/app_events'; +import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { KioskMode } from 'app/types/dashboard'; import { DashboardScene } from './DashboardScene'; -import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; describe('DashboardSceneUrlSync', () => { describe('Given a standard scene', () => { - it('Should set viewPanelKey when url has viewPanel', () => { - const scene = buildTestScene(); - scene.urlSync?.updateFromUrl({ viewPanel: '2' }); - expect(scene.state.viewPanelScene!.getUrlKey()).toBe('panel-2'); - }); - it('Should set UNSAFE_fitPanels when url has autofitpanels', () => { const scene = buildTestScene(); scene.urlSync?.updateFromUrl({ autofitpanels: '' }); @@ -58,54 +48,11 @@ describe('DashboardSceneUrlSync', () => { const scene = buildTestScene(); scene.setState({ isEditing: false }); scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1' }); - expect(scene.state.viewPanelScene).toBeDefined(); + expect(scene.state.viewPanel).toBeDefined(); scene.urlSync?.updateFromUrl({ editPanel: 'panel-1' }); expect(scene.state.editPanel).toBeDefined(); }); }); - - describe('Given a viewPanelKey with clone that is not found', () => { - const scene = buildTestScene(); - - let errorNotice = 0; - appEvents.on(AppEvents.alertError, (evt) => errorNotice++); - - scene.urlSync?.updateFromUrl({ viewPanel: 'A$panel-1' }); - - expect(scene.state.viewPanelScene).toBeUndefined(); - // Verify no error notice was shown - expect(errorNotice).toBe(0); - - // fake adding clone panel - const layout = scene.state.body as DefaultGridLayoutManager; - - layout.state.grid.setState({ - children: [ - new DashboardGridItem({ - key: 'griditem-1', - x: 0, - body: new VizPanel({ - $variables: new SceneVariableSet({ - variables: [ - new LocalValueVariable({ - name: 'server', - value: 'A', - text: 'A', - }), - ], - }), - title: 'Clone Panel A', - key: 'panel-1', - pluginId: 'table', - }), - }), - ], - }); - - // Verify it subscribes to DashboardRepeatsProcessedEvent - scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene })); - expect(scene.state.viewPanelScene?.getUrlKey()).toBe('A$panel-1'); - }); }); function buildTestScene() { diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index fcc52e46b81..4b1db8ec977 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -1,9 +1,5 @@ -import { Unsubscribable } from 'rxjs'; - -import { AppEvents } from '@grafana/data'; -import { config, locationService } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; -import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; import { KioskMode } from 'app/types/dashboard'; @@ -11,18 +7,13 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { createDashboardEditViewFor } from '../settings/utils'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; -import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId'; import { findEditPanel, getLibraryPanelBehavior } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; -import { ViewPanelScene } from './ViewPanelScene'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { - private _viewEventSub?: Unsubscribable; - constructor(private _scene: DashboardScene) {} getKeys(): string[] { @@ -34,7 +25,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { return { autofitpanels: this.getAutoFitPanels(), - viewPanel: state.viewPanelScene?.getUrlKey(), + viewPanel: state.viewPanel, editview: state.editview?.getUrlKey(), editPanel: state.editPanel?.getUrlKey() || undefined, kiosk: state.kioskMode === KioskMode.Full ? '' : undefined, @@ -52,7 +43,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } updateFromUrl(values: SceneObjectUrlValues): void { - const { viewPanelScene, isEditing, editPanel, shareView } = this._scene.state; + const { viewPanel, isEditing, editPanel, shareView } = this._scene.state; const update: Partial = {}; if (typeof values.editview === 'string' && this._scene.canEditDashboard()) { @@ -74,25 +65,9 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { // Handle view panel state if (typeof values.viewPanel === 'string') { - const panel = findVizPanelByPathId(this._scene, values.viewPanel); - - if (!panel) { - // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet - // Here we check if the key contains the clone key so we force the repeat processing - // It doesn't matter if the element or the ancestors are clones or not, just that the key contains the clone key - if (containsPathIdSeparator(values.viewPanel)) { - this._handleViewRepeatClone(values.viewPanel); - return; - } - - appEvents.emit(AppEvents.alertError, ['Panel not found']); - locationService.partial({ viewPanel: null }); - return; - } - - update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() }); - } else if (viewPanelScene && values.viewPanel === null) { - update.viewPanelScene = undefined; + update.viewPanel = values.viewPanel; + } else if (viewPanel && values.viewPanel === null) { + update.viewPanel = undefined; } // Handle edit panel state @@ -105,8 +80,8 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } // We cannot simultaneously be in edit and view panel state. - if (this._scene.state.viewPanelScene) { - this._scene.setState({ viewPanelScene: undefined }); + if (this._scene.state.viewPanel) { + update.viewPanel = undefined; } // If we are not in editing (for example after full page reload) @@ -159,21 +134,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } } - private _handleViewRepeatClone(viewPanel: string) { - if (!this._viewEventSub) { - this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { - const panel = findVizPanelByPathId(this._scene, viewPanel); - if (panel) { - this._viewEventSub?.unsubscribe(); - this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) }); - this._viewEventSub = undefined; - } - }); - - this._scene.state.body.activateRepeaters?.(); - } - } - /** * Temporary solution, with some refactoring of PanelEditor we can remove this */ diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 47fec9cf54a..18eafada208 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -66,7 +66,7 @@ NavToolbarActions.displayName = 'NavToolbarActions'; * This part is split into a separate component to help test this */ export function ToolbarActions({ dashboard }: Props) { - const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState(); + const { isEditing, viewPanel, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState(); const { isPlaying } = playlistSrv.useState(); const [isAddPanelMenuOpen, setIsAddPanelMenuOpen] = useState(false); @@ -75,7 +75,7 @@ export function ToolbarActions({ dashboard }: Props) { const toolbarActions: ToolbarAction[] = []; const styles = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); - const isViewingPanel = Boolean(viewPanelScene); + const isViewingPanel = Boolean(viewPanel); const isEditedPanelDirty = usePanelEditDirty(editPanel); const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve()); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 30774d910fb..e665be6f773 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -2,6 +2,7 @@ import { getTimeZone, InterpolateFunction, LinkModel, + locationUtil, PanelMenuItem, PanelPlugin, PluginExtensionLink, @@ -37,7 +38,7 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; -import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; +import { getEditPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; @@ -90,7 +91,10 @@ export function panelMenuBehavior(menu: VizPanelMenu) { text: t('panel.header-menu.view', `View`), iconClassName: 'eye', shortcut: 'v', - href: getViewPanelUrl(panel), + href: locationUtil.getUrlForPartial(locationService.getLocation(), { + viewPanel: panel.getPathId(), + editPanel: undefined, + }), }); } diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx index 2cf1be9dbee..f54f85fd402 100644 --- a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -1,18 +1,13 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; -import { SceneGridRow, VizPanel, sceneGraph } from '@grafana/scenes'; +import { VizPanel, sceneGraph } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; -import { forceActivateFullSceneObjectTree } from '../utils/utils'; - import { DashboardScene } from './DashboardScene'; -import { DashboardGridItem } from './layout-default/DashboardGridItem'; -import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; -import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; +import { SoloPanelContextProvider } from './SoloPanelContext'; export interface Props { dashboard: DashboardScene; @@ -24,60 +19,21 @@ const panelsPerRowCSSVar = '--panels-per-row'; export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: Props) { const { body } = dashboard.state; - const filteredPanels: VizPanel[] = []; const styles = useStyles2(getStyles); - const [_, setRepeatsUpdated] = useState(''); - - const bodyGrid = body instanceof DefaultGridLayoutManager ? body.state.grid : null; - - if (!bodyGrid) { - return Unsupported layout; - } - - for (const gridItem of bodyGrid.state.children) { - if (gridItem instanceof DashboardGridItem) { - filterPanels(gridItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); - } else if (gridItem instanceof SceneGridRow) { - for (const rowItem of gridItem.state.children) { - if (rowItem instanceof DashboardGridItem) { - filterPanels(rowItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); - } - } - } - } - - if (filteredPanels.length > 0) { - return ( -
} - > - {filteredPanels.map((panel) => ( - - ))} -
- ); - } + const soloPanelContext = useMemo(() => new SoloPanelContextValueWithSearchStringFilter(panelSearch), [panelSearch]); return ( -

- No matches found -

+
} + > + + + +
); } -function PanelSearchHit({ panel }: { panel: VizPanel }) { - useEffect(() => { - const deactivate = forceActivateFullSceneObjectTree(panel); - - return () => { - deactivate?.(); - }; - }, [panel]); - - return ; -} - function getStyles(theme: GrafanaTheme2) { return { grid: css({ @@ -96,37 +52,20 @@ function getStyles(theme: GrafanaTheme2) { }; } -function filterPanels( - gridItem: DashboardGridItem, - dashboard: DashboardScene, - searchString: string, - filteredPanels: VizPanel[], - setRepeatsUpdated: (updated: string) => void -) { - const interpolatedSearchString = sceneGraph.interpolate(dashboard, searchString).toLowerCase(); +export class SoloPanelContextValueWithSearchStringFilter { + public matchFound = false; - // activate inactive repeat panel if one of its children will be matched - if (gridItem.state.variableName && !gridItem.isActive) { - const panel = gridItem.state.body; + public constructor(private searchQuery: string) {} + + public matches(panel: VizPanel): boolean { + const interpolatedSearchString = sceneGraph.interpolate(panel, this.searchQuery).toLowerCase(); const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase(); - if (interpolatedTitle.includes(interpolatedSearchString)) { - gridItem.subscribeToEvent(DashboardRepeatsProcessedEvent, (event) => { - const source = event.payload.source; - if (source instanceof DashboardGridItem) { - setRepeatsUpdated(event.payload.source.state.key ?? ''); - } - }); - gridItem.activate(); - } - } - const panels = gridItem.state.repeatedPanels ?? [gridItem.state.body]; - for (const panel of panels) { - const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase(); - if (interpolatedTitle.includes(interpolatedSearchString)) { - filteredPanels.push(panel); + const match = interpolatedTitle.includes(interpolatedSearchString); + if (match) { + this.matchFound = true; } - } - return filteredPanels; + return match; + } } diff --git a/public/app/features/dashboard-scene/scene/SoloPanelContext.tsx b/public/app/features/dashboard-scene/scene/SoloPanelContext.tsx new file mode 100644 index 00000000000..2186d9b4863 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/SoloPanelContext.tsx @@ -0,0 +1,141 @@ +import React, { useContext, useEffect, useState } from 'react'; + +import { Trans } from '@grafana/i18n'; +import { VizPanel } from '@grafana/scenes'; +import { Box, Spinner } from '@grafana/ui'; + +import { DashboardScene } from './DashboardScene'; + +export interface SoloPanelContextValue { + matches: (VizPanel: VizPanel) => boolean; + matchFound: boolean; +} + +export class SoloPanelContextWithPathIdFilter implements SoloPanelContextValue { + public matchFound = false; + + public constructor(public keyPath: string) {} + + public matches(panel: VizPanel): boolean { + // Check if keyPath is just an old legacy panel id + if (/^\d+$/.test(this.keyPath)) { + if (`panel-${this.keyPath}` === panel.state.key!) { + this.matchFound = true; + return true; + } + + return false; + } + + if (this.keyPath === panel.getPathId()) { + this.matchFound = true; + return true; + } + + return false; + } +} + +export const SoloPanelContext = React.createContext(null); + +export function useDefineSoloPanelContext(keyPath?: string): SoloPanelContextValue | null { + return React.useMemo(() => { + if (!keyPath) { + return null; + } + return new SoloPanelContextWithPathIdFilter(keyPath); + }, [keyPath]); +} + +export function useSoloPanelContext() { + return useContext(SoloPanelContext); +} + +export function renderMatchingSoloPanels(soloPanelContext: SoloPanelContextValue, panels: VizPanel[]) { + const matches: React.ReactNode[] = []; + for (const panel of panels) { + if (soloPanelContext.matches(panel)) { + matches.push(); + } + } + + return <>{matches}; +} + +export function SoloPanelContextProvider({ + children, + value, + singleMatch, + dashboard, +}: { + children: React.ReactNode; + value: SoloPanelContextValue; + singleMatch: boolean; + dashboard: DashboardScene; +}) { + return ( + + {children} + + + ); +} + +export interface SoloPanelNotFoundProps { + /** + * Controls panel not found error message + */ + singleMatch: boolean; + /** + * Used to check if variables are loading + */ + dashboard: DashboardScene; +} + +export function SoloPanelNotFound({ singleMatch, dashboard }: SoloPanelNotFoundProps) { + const context = useSoloPanelContext()!; + const [state, setState] = useState({ matchFound: false, isLoading: true }); + + useEffect(() => { + // This effect fires before any child layout starts rendering and checking if their panels match the solo panel filter + // We need this polling here to check if any solo panel has matched or if any layout has marked the context as loading (for repeated panels) + const cancelTimeout = setInterval(() => { + setState({ matchFound: context.matchFound, isLoading: isAnyVariableLoading(dashboard) }); + }, 500); + + return () => clearInterval(cancelTimeout); + }, [context, dashboard]); + + if (state.matchFound || context.matchFound) { + return null; + } + + if (state.isLoading) { + return ; + } + + return ( + + {singleMatch && Panel not found} + {!singleMatch && No panels matching} + + ); +} + +function isAnyVariableLoading(scene: DashboardScene) { + const variables = scene.state.$variables; + if (!variables || !variables.isActive) { + return true; + } + + return variables.state.variables.some((variable) => variable.state.loading); +} diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index bf3872b2603..3284dd50e51 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -13,7 +13,7 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { findVizPanelByPathId } from '../utils/pathId'; -import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; +import { getEditPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; @@ -50,15 +50,10 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { keybindings.addBinding({ key: 'v', onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { - if (scene.state.viewPanelScene) { - locationService.push( - locationUtil.getUrlForPartial(locationService.getLocation(), { - viewPanel: undefined, - }) - ); + if (scene.state.viewPanel) { + locationService.partial({ viewPanel: undefined }); } else { - const url = locationUtil.stripBaseFromUrl(getViewPanelUrl(vizPanel)); - locationService.push(url); + locationService.partial({ viewPanel: vizPanel.getPathId(), editPanel: undefined }); } }), }); diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx index 6f6d211777b..2235985ba7e 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx @@ -18,7 +18,6 @@ import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; -import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { getOptions } from './AutoGridItemEditor'; import { AutoGridItemRenderer } from './AutoGridItemRenderer'; @@ -130,8 +129,6 @@ export class AutoGridItem extends SceneObjectBase implements this.setState({ repeatedPanels }); this._prevRepeatValues = values; - - this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); } public getPanelCount() { diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx index b64747e26d1..12903f1e727 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItemRenderer.tsx @@ -7,6 +7,7 @@ import { useStyles2 } from '@grafana/ui'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; import { useDashboardState } from '../../utils/utils'; +import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext'; import { getIsLazy } from '../layouts-shared/utils'; import { AutoGridItem } from './AutoGridItem'; @@ -19,7 +20,7 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps getIsLazy(preload), [preload]); const Wrapper = useMemo( @@ -77,6 +78,10 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps ); + } + return (
) { const { repeatedPanels = [], itemHeight, variableName, body } = model.useState(); + const soloPanelContext = useSoloPanelContext(); const layoutStyle = useLayoutStyle( model.getRepeatDirection(), model.getPanelCount(), @@ -16,6 +19,10 @@ export function DashboardGridItemRenderer({ model }: SceneComponentProps diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 97c04cbdd86..ad3930e1aab 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -42,6 +42,7 @@ import { getLayoutOrchestratorFor, getDashboardSceneFor, } from '../../utils/utils'; +import { useSoloPanelContext } from '../SoloPanelContext'; import { AutoGridItem } from '../layout-auto-grid/AutoGridItem'; import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions'; import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-shared/paste'; @@ -565,6 +566,11 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps ); + } // If we are top level layout and we have no children, show empty state if (model.parent === dashboard && children.length === 0) { @@ -585,6 +591,20 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps) { + const soloPanelContext = useSoloPanelContext(); + + if (soloPanelContext) { + return model.state.children.map((child) => ); + } + + return ; +} + function getStyles(theme: GrafanaTheme2) { return { container: css({ diff --git a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts index a813fddaace..5a6553dd941 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts @@ -14,7 +14,6 @@ import { import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; -import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; interface RowRepeaterBehaviorState extends SceneObjectState { variableName: string; @@ -172,9 +171,6 @@ export class RowRepeaterBehavior extends SceneObjectBase) { const clearStyles = useStyles2(clearButtonStyles); const isTopLevel = model.parent?.parent instanceof DashboardScene; const pointerDistance = usePointerDistance(); + const soloPanelContext = useSoloPanelContext(); const myIndex = rows.findIndex((row) => row === model); @@ -45,6 +47,10 @@ export function RowItemRenderer({ model }: SceneComponentProps) { return null; } + if (soloPanelContext) { + return ; + } + return ( {(dragProvided, dragSnapshot) => ( diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx index 39681c4fa4c..0e18ffd7ef0 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx @@ -7,7 +7,6 @@ import { Spinner } from '@grafana/ui'; import { DashboardStateChangedEvent } from '../../edit-pane/shared'; import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { dashboardLog, getMultiVariableValues } from '../../utils/utils'; -import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { RowItem } from './RowItem'; import { RowsLayoutManager } from './RowsLayoutManager'; @@ -118,7 +117,6 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co } row.setState({ repeatedRows: clonedRows }); - row.publishEvent(new DashboardRepeatsProcessedEvent({ source: row }), true); } /** diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx index 523b44c6422..b18634bdd00 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx @@ -9,6 +9,7 @@ import { Button, useStyles2 } from '@grafana/ui'; import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; +import { useSoloPanelContext } from '../SoloPanelContext'; import { useClipboardState } from '../layouts-shared/useClipboardState'; import { RowItem } from './RowItem'; @@ -20,6 +21,11 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps ); + } const isClone = isRepeatCloneOrChildOf(model); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx index 16ac618b600..ae545b9488e 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx @@ -9,7 +9,6 @@ import { Spinner, Tooltip, useStyles2 } from '@grafana/ui'; import { DashboardStateChangedEvent } from '../../edit-pane/shared'; import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { dashboardLog, getMultiVariableValues } from '../../utils/utils'; -import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; import { TabItem } from './TabItem'; import { TabsLayoutManager } from './TabsLayoutManager'; @@ -99,7 +98,6 @@ export function performTabRepeats(variable: MultiValueVariable, tab: TabItem, co const clonedTabs = createTabRepeats({ values, texts, variable, tab }); tab.setState({ repeatedTabs: clonedTabs }); - tab.publishEvent(new DashboardRepeatsProcessedEvent({ source: tab }), true); } /** diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx index a223350371b..b5a6d11a0cd 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx @@ -10,6 +10,7 @@ import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; +import { useSoloPanelContext } from '../SoloPanelContext'; import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles'; import { useClipboardState } from '../layouts-shared/useClipboardState'; @@ -26,6 +27,11 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps; + } const isClone = isRepeatCloneOrChildOf(model); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx index 6f7f361caf6..211870823b4 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx @@ -10,10 +10,10 @@ import { StarButton } from './actions/StarButton'; import { getDynamicActions, renderActionElements } from './utils'; export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => { - const { editview, editPanel, isEditing, uid, meta, viewPanelScene } = dashboard.useState(); + const { editview, editPanel, isEditing, uid, meta, viewPanel } = dashboard.useState(); const hasEditView = Boolean(editview); - const isViewingPanel = Boolean(viewPanelScene); + const isViewingPanel = Boolean(viewPanel); const isEditingDashboard = Boolean(isEditing); const isEditingPanel = Boolean(editPanel); const hasUid = Boolean(uid); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx index 76cb969eeaf..d737abf783c 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx @@ -27,7 +27,7 @@ import { ToolbarActionProps } from './types'; import { getDynamicActions, renderActionElements } from './utils'; export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { - const { editPanel, editable, editview, isEditing, uid, meta, viewPanelScene } = dashboard.useState(); + const { editPanel, editable, editview, isEditing, uid, meta, viewPanel } = dashboard.useState(); const { isPlaying } = playlistSrv.useState(); const styles = useStyles2(getStyles); @@ -37,7 +37,7 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { const isEditingDashboard = Boolean(isEditing); const hasEditView = Boolean(editview); const isEditingPanel = Boolean(editPanel); - const isViewingPanel = Boolean(viewPanelScene); + const isViewingPanel = Boolean(viewPanel); const isEditingLibraryPanel = isEditingPanel && isLibraryPanel(editPanel!.state.panelRef.resolve()); const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel; const isEditingAndShowingDashboard = isEditingDashboard && isShowingDashboard; diff --git a/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts b/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts deleted file mode 100644 index 9bf602cc171..00000000000 --- a/public/app/features/dashboard-scene/scene/types/DashboardRepeatsProcessedEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BusEventWithPayload } from '@grafana/data'; -import { SceneObject } from '@grafana/scenes'; - -export interface DashboardRepeatsProcessedEventPayload { - source: SceneObject; -} - -export class DashboardRepeatsProcessedEvent extends BusEventWithPayload { - public static type = 'dashboard-repeats-processed'; -} diff --git a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx index e38fd63e5d1..f94152f3b99 100644 --- a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx +++ b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx @@ -4,9 +4,9 @@ import { useEffect } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; +import { t } from '@grafana/i18n'; import { UrlSyncContextProvider } from '@grafana/scenes'; -import { Alert, Box, Spinner, useStyles2 } from '@grafana/ui'; +import { Alert, Box, useStyles2 } from '@grafana/ui'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -15,8 +15,7 @@ import { DashboardRoutes } from 'app/types/dashboard'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; - -import { useSoloPanel } from './useSoloPanel'; +import { SoloPanelContextProvider, useDefineSoloPanelContext } from '../scene/SoloPanelContext'; export interface Props extends GrafanaRouteComponentProps {} @@ -61,30 +60,26 @@ export function SoloPanelPage({ queryParams }: Props) { export default SoloPanelPage; export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: DashboardScene; panelId: string }) { - const [panel, error] = useSoloPanel(dashboard, panelId); - const { controls } = dashboard.useState(); + const { controls, body } = dashboard.useState(); const refreshPicker = controls?.useState()?.refreshPicker; const styles = useStyles2(getStyles); + const soloPanelContext = useDefineSoloPanelContext(panelId)!; useEffect(() => { - return refreshPicker?.activate(); - }, [refreshPicker]); + const dashDeactivate = dashboard.activate(); + const refreshDeactivate = refreshPicker?.activate(); - if (error) { - return ; - } - - if (!panel) { - return ( - - Loading - - ); - } + return () => { + dashDeactivate(); + refreshDeactivate?.(); + }; + }, [dashboard, refreshPicker]); return (
- + + +
); } diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx b/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx deleted file mode 100644 index 86b2a8a1ab6..00000000000 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { renderHook } from '@testing-library/react'; - -import { DataSourceRef } from '@grafana/schema'; - -import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; -import { findVizPanelByKey } from '../utils/utils'; - -import { useSoloPanel } from './useSoloPanel'; - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getDataSourceSrv: () => ({ - get: async (ref: DataSourceRef) => { - // Mocking the build in Grafana data source to avoid annotations data layer errors. - return { - id: 1, - uid: '-- Grafana --', - name: 'grafana', - type: 'grafana', - meta: { - id: 'grafana', - }, - }; - }, - }), -})); - -describe('useSoloPanel', () => { - it('should return undefined panel and error when panel is not found', () => { - const { dashboard } = setup(); - const { result } = renderHook(() => useSoloPanel(dashboard, 'foo-key')); - - expect(result.current[0]).toBeUndefined(); - expect(result.current[1]).toBe('Panel not found'); - }); - - it('should return the panel when panel is found', () => { - const { dashboard } = setup(); - - const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1')); - const panel = findVizPanelByKey(dashboard, 'panel-1'); - - expect(result.current[0]).toEqual(panel); - expect(result.current[1]).toBeUndefined(); - }); - - it('should return the cloned panel when panel is found', () => { - const { dashboard } = setup(); - const { result } = renderHook(() => useSoloPanel(dashboard, 'A$panel-1')); - const panel = findVizPanelByKey(dashboard, 'panel-1'); - - expect(result.current[0]).not.toBe(panel); - expect(result.current[1]).toBeUndefined(); - }); - - it('should return error when panelId correspond to a non VizPanel', () => { - const { dashboard } = setup(); - const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-2')); - - expect(result.current[0]).toBeUndefined(); - expect(result.current[1]).toBe('Panel not found'); - }); -}); - -const setup = () => { - const dashboard = transformSaveModelToScene({ dashboard: TEST_DASHBOARD, meta: {} }); - - return { dashboard }; -}; - -const TEST_DASHBOARD = { - title: 'Scenes/PanelEdit/Queries: Edit', - annotations: { - list: [], - }, - editable: true, - fiscalYearStartMonth: 0, - graphTooltip: 0, - id: 2378, - links: [], - liveNow: false, - panels: [ - { - type: 'timeseries', - datasource: 'prometheus', - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - gridPos: { - h: 9, - w: 24, - x: 0, - y: 0, - }, - id: 1, - options: { - colorMode: 'background', - graphMode: 'area', - justifyMode: 'auto', - orientation: 'auto', - reduceOptions: { - calcs: ['lastNotNull', 'last', 'first', 'min', 'max', 'mean', 'sum', 'count'], - fields: '', - values: false, - }, - text: {}, - }, - pluginVersion: '8.0.3', - }, - { - id: 2, - type: 'row', - }, - ], - refresh: '', - schemaVersion: 39, - tags: [], - templating: { - list: [], - }, - time: { - from: 'now-6h', - to: 'now', - }, - timepicker: {}, - timezone: '', - uid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', - version: 6, - weekStart: '', -}; diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts deleted file mode 100644 index 37777439b4c..00000000000 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { VizPanel, UrlSyncManager } from '@grafana/scenes'; - -import { DashboardScene } from '../scene/DashboardScene'; -import { DashboardRepeatsProcessedEvent } from '../scene/types/DashboardRepeatsProcessedEvent'; -import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId'; - -export function useSoloPanel(dashboard: DashboardScene, pathId: string): [VizPanel | undefined, string | undefined] { - const [panel, setPanel] = useState(); - const [error, setError] = useState(); - - useEffect(() => { - const urlSyncManager = new UrlSyncManager(); - urlSyncManager.initSync(dashboard); - - const cleanUp = dashboard.activate(); - - let panel: VizPanel | null = null; - try { - panel = findVizPanelByPathId(dashboard, pathId); - } catch (e) { - // do nothing, just the panel is not found or not a VizPanel - } - - if (panel) { - activateParents(panel); - setPanel(panel); - } else if (containsPathIdSeparator(pathId)) { - findRepeatClone(dashboard, pathId).then((panel) => { - if (panel) { - setPanel(panel); - } else { - setError('Panel not found'); - } - }); - } else { - setError('Panel not found'); - } - - return cleanUp; - }, [dashboard, pathId]); - - return [panel, error]; -} - -function activateParents(panel: VizPanel) { - let parent = panel.parent; - - while (parent && !parent.isActive) { - parent.activate(); - parent = parent.parent; - } -} - -function findRepeatClone(dashboard: DashboardScene, pathId: string): Promise { - return new Promise((resolve) => { - dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { - const panel = findVizPanelByPathId(dashboard, pathId); - if (panel) { - resolve(panel); - } else { - // If rows are repeated they could add new panel repeaters that needs to be activated - dashboard.state.body.activateRepeaters?.(); - } - }); - - dashboard.state.body.activateRepeaters?.(); - }); -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c088579bcb7..b5a8cf1376d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5223,6 +5223,9 @@ "save-json-to-file": "Save JSON to file", "see-docs": "See <2>documentation for more information about provisioning." }, + "search-panel": { + "no-match": "No panels matching" + }, "share-public-dashboard-loader": { "loading-configuration": "Loading configuration" }, @@ -5495,6 +5498,9 @@ "view-json-modal": { "title-json": "JSON" }, + "view-panel": { + "not-found": "Panel not found" + }, "visualization-button": { "aria-label-change-visualization": "Change visualization", "aria-label-close": "Close options pane", @@ -6135,9 +6141,6 @@ "share-button": { "aria-label-sharedropdownmenu": "Toggle share menu" }, - "solo-panel-page": { - "loading": "Loading" - }, "support-snapshot-service": { "description": { "dashboard-troubleshoot-visualization-issues": "Dashboard JSON used to help troubleshoot visualization issues" @@ -10712,10 +10715,6 @@ "could-anything-matching-query": "Could not find anything matching your query" } }, - "panel-search": { - "no-matches": "No matches found", - "unsupported-layout": "Unsupported layout" - }, "panel-type-filter": { "clear-button": "Clear types", "select-aria-label": "Panel type filter",