Dashboard: Inspect drawer can no longer be opened with url or linked to (#109617)

* Inspect: Remove url state support

* Fixes

* fix e2e

* fix lint

* fix lint

* fix test

* Update
This commit is contained in:
Torkel Ödegaard
2025-08-15 13:32:19 +02:00
committed by GitHub
parent 8046a78f9f
commit 68208577d3
13 changed files with 35 additions and 222 deletions
@@ -61,8 +61,7 @@ test.describe(
await expect(queryTab).toBeVisible();
// Query should be the active tab
const activeTab = page.locator('a[class*="-activeTabStyle"]');
await expect(activeTab).toHaveText('Query');
await expect(queryTab).toHaveClass(/.*-activeTabStyle/);
const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content);
await expect(queryContent).toBeVisible();
@@ -1,76 +0,0 @@
import { locationService } from '@grafana/runtime';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { onPanelInspectClose } from './PanelInspectDrawer';
describe('onPanelInspectClose', () => {
test('when on default home dashboard page', async () => {
locationService.push('/');
const { scene } = await buildTestScene({
url: '',
slug: '',
});
onPanelInspectClose(scene);
expect(locationService.getLocation().pathname).toBe('/');
});
test('when on custom home dashboard page with uid defined', async () => {
locationService.push('/');
const { scene } = await buildTestScene(
{
url: '',
slug: '',
},
'home-dash '
);
onPanelInspectClose(scene);
expect(locationService.getLocation().pathname).toBe('/');
});
test('when on new dashboard page', async () => {
locationService.push('/dashboard/new');
const { scene } = await buildTestScene(
{
url: '',
slug: '',
},
''
);
onPanelInspectClose(scene);
expect(locationService.getLocation().pathname).toBe('/dashboard/new');
});
test('when on a dashboard page', async () => {
const { scene } = await buildTestScene(
{
slug: 'dash-slug',
url: '/d/dash-uid/dash-slug',
},
'dash-uid'
);
onPanelInspectClose(scene);
expect(locationService.getLocation().pathname).toBe('/d/dash-uid/dash-slug');
});
});
async function buildTestScene(metaOverride?: DashboardSceneState['meta'], uid = 'dash-1') {
const scene = new DashboardScene({
title: 'hello',
uid,
meta: {
canEdit: true,
...metaOverride,
},
body: DefaultGridLayoutManager.fromVizPanels([]),
});
return { scene };
}
@@ -1,8 +1,4 @@
import { useLocation } from 'react-router-dom-v5-compat';
import { locationUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { locationService } from '@grafana/runtime';
import {
SceneComponentProps,
SceneObjectBase,
@@ -16,8 +12,6 @@ import { getDataSourceWithInspector } from 'app/features/dashboard/components/In
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { InspectTab } from 'app/features/inspector/types';
import { DashboardScene } from '../scene/DashboardScene';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { getDashboardSceneFor } from '../utils/utils';
import { HelpWizard } from './HelpWizard/HelpWizard';
@@ -30,6 +24,7 @@ import { SceneInspectTab } from './types';
interface PanelInspectDrawerState extends SceneObjectState {
tabs?: SceneInspectTab[];
currentTab: InspectTab;
panelRef: SceneObjectRef<VizPanel>;
pluginNotLoaded?: boolean;
canEdit?: boolean;
@@ -93,26 +88,21 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
}
onClose = () => {
onPanelInspectClose(getDashboardSceneFor(this));
getDashboardSceneFor(this).closeModal();
};
}
function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>) {
const { tabs, pluginNotLoaded, panelRef } = model.useState();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const { tabs, pluginNotLoaded, panelRef, currentTab } = model.useState();
if (!tabs) {
return null;
}
const urlTab = queryParams.get('inspectTab');
const currentTab = tabs.find((tab) => tab.getTabValue() === urlTab) ?? tabs[0];
const vizPanel = panelRef!.resolve();
const activeTab = tabs.find((tab) => tab.getTabValue() === currentTab) ?? tabs[0];
if (urlTab === InspectTab.Help) {
if (currentTab === InspectTab.Help) {
return <HelpWizard panel={vizPanel} onClose={model.onClose} />;
}
@@ -128,8 +118,10 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
<Tab
key={tab.state.key!}
label={tab.getTabLabel()}
active={tab === currentTab}
href={locationUtil.getUrlForPartial(location, { inspectTab: tab.getTabValue() })}
active={tab === activeTab}
onChangeTab={() => {
model.setState({ currentTab: tab.getTabValue() });
}}
/>
);
})}
@@ -145,27 +137,7 @@ function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>
</Trans>
</Alert>
)}
{currentTab && currentTab.Component && <currentTab.Component model={currentTab} />}
{activeTab.Component && <activeTab.Component model={activeTab} />}
</Drawer>
);
}
export function onPanelInspectClose(dashboard: DashboardScene) {
const meta = dashboard.state.meta;
// Checking for location here as well, otherwise down below isHomeDashboard will be set to true
// as it doesn't have uid neither slug nor url.
const isNew = !dashboard.state.uid && locationService.getLocation().pathname === '/dashboard/new';
locationService.push(
getDashboardUrl({
uid: dashboard.state.uid,
slug: dashboard.state.meta.slug,
currentQueryParams: locationService.getLocation().search,
updateQuery: {
inspect: null,
inspectTab: null,
},
isHomeDashboard: !meta.url && !meta.slug && !isNew,
})
);
}
@@ -212,7 +212,7 @@ describe('DashboardScenePage', () => {
expect(await screen.findByText('Inspect: Panel B')).toBeInTheDocument();
act(() => locationService.partial({ inspect: null }));
await userEvent.click(screen.getByTestId(selectors.components.Drawer.General.close));
expect(screen.queryByText('Inspect: Panel B')).not.toBeInTheDocument();
});
@@ -18,12 +18,12 @@ import {
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard';
import { InspectTab } from 'app/features/inspector/types';
import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants';
import { DashboardDataDTO } from 'app/types/dashboard';
import { PanelInspectDrawer } from '../../inspect/PanelInspectDrawer';
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange';
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
import { findVizPanelByKey } from '../../utils/utils';
@@ -604,12 +604,10 @@ describe('PanelDataQueriesTab', () => {
describe('query inspection', () => {
it('allows query inspection from the tab', async () => {
const { queriesTab } = await setupScene('panel-1');
const { queriesTab, scene } = await setupScene('panel-1');
queriesTab.onOpenInspector();
const params = locationService.getSearchObject();
expect(params.inspect).toBe('1');
expect(params.inspectTab).toBe(InspectTab.Query);
expect(scene.state.overlay).toBeInstanceOf(PanelInspectDrawer);
});
});
@@ -1,7 +1,7 @@
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
import { config, getDataSourceSrv } from '@grafana/runtime';
import {
SceneObjectBase,
SceneComponentProps,
@@ -21,6 +21,7 @@ import { dataSource as expressionDatasource } from 'app/features/expressions/Exp
import { ExpressionTypeDropdown } from 'app/features/expressions/components/ExpressionTypeDropdown';
import { ExpressionQueryType } from 'app/features/expressions/types';
import { getDefaults } from 'app/features/expressions/utils/expressionTypes';
import { InspectTab } from 'app/features/inspector/types';
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
@@ -32,8 +33,9 @@ import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/Mixe
import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext';
import { ExpressionDatasourceUID } from '../../../expressions/types';
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
import { PanelInspectDrawer } from '../../inspect/PanelInspectDrawer';
import { PanelTimeRange } from '../../scene/PanelTimeRange';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils';
import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
import { getUpdatedHoverHeader } from '../getPanelFrameOptions';
import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
@@ -171,10 +173,8 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
}
public onOpenInspector = () => {
const panel = this.state.panelRef.resolve();
const panelId = getPanelIdForVizPanel(panel);
locationService.partial({ inspect: panelId, inspectTab: 'query' });
const dashboard = getDashboardSceneFor(this);
dashboard.showModal(new PanelInspectDrawer({ panelRef: this.state.panelRef, currentTab: InspectTab.Query }));
};
public onChangeDataSource = async (newSettings: DataSourceInstanceSettings, defaultQueries?: SceneDataQuery[]) => {
@@ -129,8 +129,6 @@ export interface DashboardSceneState extends SceneObjectState {
meta: Omit<DashboardMeta, 'isNew'>;
/** Version of the dashboard */
version?: number;
/** Panel to inspect */
inspectPanelKey?: string;
/** Panel to view in fullscreen */
viewPanelScene?: ViewPanelScene;
/** Edit view */
@@ -12,18 +12,6 @@ import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcesse
describe('DashboardSceneUrlSync', () => {
describe('Given a standard scene', () => {
it('Should set inspectPanelKey when url has inspect key', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '2' });
expect(scene.state.inspectPanelKey).toBe('2');
});
it('Should handle inspect key that is not found', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '12321' });
expect(scene.state.inspectPanelKey).toBe(undefined);
});
it('Should set viewPanelKey when url has viewPanel', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
@@ -7,7 +7,6 @@ import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core';
import { KioskMode } from 'app/types/dashboard';
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createDashboardEditViewFor } from '../settings/utils';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
@@ -23,7 +22,6 @@ import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcesse
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
private _viewEventSub?: Unsubscribable;
private _inspectEventSub?: Unsubscribable;
constructor(private _scene: DashboardScene) {}
@@ -35,7 +33,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
const state = this._scene.state;
return {
inspect: state.inspectPanelKey,
autofitpanels: this.getAutoFitPanels(),
viewPanel: state.viewPanelScene?.getUrlKey(),
editview: state.editview?.getUrlKey(),
@@ -55,7 +52,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
}
updateFromUrl(values: SceneObjectUrlValues): void {
const { inspectPanelKey, viewPanelScene, isEditing, editPanel, shareView } = this._scene.state;
const { viewPanelScene, isEditing, editPanel, shareView } = this._scene.state;
const update: Partial<DashboardSceneState> = {};
if (typeof values.editview === 'string' && this._scene.canEditDashboard()) {
@@ -75,30 +72,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
update.editview = undefined;
}
// Handle inspect object state
if (typeof values.inspect === 'string') {
let panel = findVizPanelByKey(this._scene, values.inspect);
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 (containsCloneKey(values.inspect)) {
this._handleInspectRepeatClone(values.inspect);
return;
}
appEvents.emit(AppEvents.alertError, ['Panel not found']);
locationService.partial({ inspect: null });
return;
}
update.inspectPanelKey = values.inspect;
update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() });
} else if (inspectPanelKey) {
update.inspectPanelKey = undefined;
update.overlay = undefined;
}
// Handle view panel state
if (typeof values.viewPanel === 'string') {
const panel = findVizPanelByKey(this._scene, values.viewPanel);
@@ -186,21 +159,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
}
}
private _handleInspectRepeatClone(inspect: string) {
if (!this._inspectEventSub) {
this._inspectEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
const panel = findVizPanelByKey(this._scene, inspect);
if (panel) {
this._inspectEventSub?.unsubscribe();
this._scene.setState({
inspectPanelKey: inspect,
overlay: new PanelInspectDrawer({ panelRef: panel.getRef() }),
});
}
});
}
}
private _handleViewRepeatClone(viewPanel: string) {
if (!this._viewEventSub) {
this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
@@ -90,8 +90,7 @@ describe('panelMenuBehavior', () => {
expect(getExploreArgs.queries).toEqual([{ query: 'QueryA', refId: 'A' }]);
expect(getExploreArgs.scopedVars?.__sceneObject?.value).toBe(panel);
// verify inspect url keeps url params and adds inspect=<panel-key>
expect(menu.state.items?.[4].href).toBe('/d/dash-1?from=now-5m&to=now&inspect=panel-12');
expect(menu.state.items?.[4].text).toBe('Inspect');
expect(menu.state.items?.[4].subMenu).toBeDefined();
expect(menu.state.items?.[4].subMenu?.length).toBe(3);
@@ -32,11 +32,12 @@ import { dispatch } from 'app/store/store';
import { AccessControlAction } from 'app/types/accessControl';
import { ShowConfirmModalEvent } from 'app/types/events';
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
import { isInCloneChain } from '../utils/clone';
import { DashboardInteractions } from '../utils/interactions';
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
@@ -290,7 +291,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
iconClassName: 'question-circle',
onClick: (e: React.MouseEvent) => {
e.preventDefault();
onInspectPanel(panel, InspectTab.Help);
dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Help }));
},
});
}
@@ -404,20 +405,18 @@ function getInspectMenuItem(
if (plugin && !plugin.meta.skipDataQuery) {
inspectSubMenu.push({
text: t('panel.header-menu.inspect-data', `Data`),
href: getInspectUrl(panel, InspectTab.Data),
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data });
dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Data }));
},
});
if (dashboard instanceof DashboardScene && dashboard.state.meta.canEdit) {
inspectSubMenu.push({
text: t('panel.header-menu.query', `Query`),
href: getInspectUrl(panel, InspectTab.Query),
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Query });
dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Query }));
},
});
}
@@ -425,10 +424,9 @@ function getInspectMenuItem(
inspectSubMenu.push({
text: t('panel.header-menu.inspect-json', `Panel JSON`),
href: getInspectUrl(panel, InspectTab.JSON),
onClick: (e) => {
e.preventDefault();
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.JSON });
dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.JSON }));
},
});
@@ -436,10 +434,9 @@ function getInspectMenuItem(
text: t('panel.header-menu.inspect', `Inspect`),
iconClassName: 'info-circle',
shortcut: 'i',
href: getInspectUrl(panel),
onClick: (e) => {
if (!e.isDefaultPrevented()) {
locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data });
dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Data }));
}
},
subMenu: inspectSubMenu.length > 0 ? inspectSubMenu : undefined,
@@ -582,10 +579,3 @@ export function toggleVizPanelLegend(vizPanel: VizPanel): void {
function hasLegendOptions(optionsWithLegend: unknown): optionsWithLegend is OptionsWithLegend {
return optionsWithLegend != null && typeof optionsWithLegend === 'object' && 'legend' in optionsWithLegend;
}
const onInspectPanel = (vizPanel: VizPanel, tab?: InspectTab) => {
locationService.partial({
inspect: vizPanel.state.key,
inspectTab: tab,
});
};
@@ -4,13 +4,15 @@ import { sceneGraph, VizPanel } from '@grafana/scenes';
import appEvents from 'app/core/app_events';
import { KeybindingSet } from 'app/core/services/KeybindingSet';
import { contextSrv } from 'app/core/services/context_srv';
import { InspectTab } from 'app/features/inspector/types';
import { AccessControlAction } from 'app/types/accessControl';
import { shareDashboardType } from '../../dashboard/components/ShareModal/utils';
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getPanelIdForVizPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
@@ -111,15 +113,7 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
keybindings.addBinding({
key: 'i',
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
if (scene.state.inspectPanelKey) {
locationService.push(
locationUtil.getUrlForPartial(locationService.getLocation(), {
inspect: undefined,
})
);
} else {
locationService.push(locationUtil.stripBaseFromUrl(getInspectUrl(vizPanel)));
}
scene.showModal(new PanelInspectDrawer({ panelRef: vizPanel.getRef(), currentTab: InspectTab.Data }));
}),
});
@@ -3,7 +3,6 @@ import { locationService } from '@grafana/runtime';
import { sceneGraph, VizPanel } from '@grafana/scenes';
import { contextSrv } from 'app/core/core';
import { getExploreUrl } from 'app/core/utils/explore';
import { InspectTab } from 'app/features/inspector/types';
import { getQueryRunnerFor } from './utils';
@@ -18,12 +17,6 @@ export function getEditPanelUrl(panelId: number) {
return locationUtil.getUrlForPartial(locationService.getLocation(), { editPanel: panelId, viewPanel: undefined });
}
export function getInspectUrl(vizPanel: VizPanel, inspectTab?: InspectTab) {
const inspect = vizPanel.state.key?.replace('-view', '');
return locationUtil.getUrlForPartial(locationService.getLocation(), { inspect, inspectTab });
}
export function tryGetExploreUrlForPanel(vizPanel: VizPanel): Promise<string | undefined> {
//const dashboard = panel.getRoot();
const panelPlugin = vizPanel.getPlugin();