From 4738957360e80e245c02057d9cfce49ee981fc72 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:51:23 +0200 Subject: [PATCH] Querying: Pass dashboard and panel title as headers (#107032) * WIP dashboard titile * Add tests * Fix tests * Fix tests * Remove redundant imports --- packages/grafana-data/src/types/datasource.ts | 1 + .../src/utils/DataSourceWithBackend.test.ts | 53 +++++++++++++++++++ .../src/utils/DataSourceWithBackend.ts | 8 +++ .../scene/DashboardScene.test.tsx | 2 + .../dashboard-scene/scene/DashboardScene.tsx | 1 + .../PanelEditor/PanelEditorTableView.test.tsx | 2 + .../PanelEditor/PanelEditorTableView.tsx | 1 + .../dashboard/dashgrid/PanelStateWrapper.tsx | 1 + .../features/dashboard/state/PanelModel.ts | 4 +- .../features/query/state/PanelQueryRunner.ts | 3 ++ .../datasource/loki/datasource.test.ts | 36 ------------- .../app/plugins/datasource/loki/datasource.ts | 27 ---------- 12 files changed, 75 insertions(+), 64 deletions(-) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 03e086dffdb..9e7f8625284 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -574,6 +574,7 @@ export interface DataQueryRequest { panelName?: string; panelPluginId?: string; dashboardUID?: string; + dashboardTitle?: string; headers?: Record; /** Filters to dynamically apply to all queries */ diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index e249d727a68..2d0ea1b19c6 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -227,6 +227,59 @@ describe('DataSourceWithBackend', () => { `); }); + test('correctly passes dashboard and panel headers', () => { + const { mock, ds } = createMockDatasource(); + ds.query({ + maxDataPoints: 10, + intervalMs: 5000, + targets: [{ refId: 'A' }], + dashboardUID: 'dashA', + dashboardTitle: 'My Test Dashboard', + panelId: 123, + panelName: 'CPU Usage Panel', + range: getDefaultTimeRange(), + } as DataQueryRequest); + + const args = mock.calls[0][0]; + + expect(mock.calls.length).toBe(1); + expect(args).toMatchInlineSnapshot(` + { + "data": { + "from": "1697133600000", + "queries": [ + { + "applyTemplateVariablesCalled": true, + "datasource": { + "type": "dummy", + "uid": "abc", + }, + "datasourceId": 1234, + "filters": undefined, + "intervalMs": 5000, + "maxDataPoints": 10, + "queryCachingTTL": undefined, + "refId": "A", + }, + ], + "to": "1697155200000", + }, + "headers": { + "X-Dashboard-Title": "My Test Dashboard", + "X-Dashboard-Uid": "dashA", + "X-Datasource-Uid": "abc", + "X-Panel-Id": "123", + "X-Panel-Title": "CPU Usage Panel", + "X-Plugin-Id": "dummy", + }, + "hideFromInspector": false, + "method": "POST", + "requestId": undefined, + "url": "/api/ds/query?ds_type=dummy", + } + `); + }); + test('correctly creates expression queries', () => { const { mock, ds } = createMockDatasource(); ds.query({ diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index d9ed30eee0a..ccfe41b0c9d 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -87,6 +87,8 @@ enum PluginRequestHeaders { QueryGroupID = 'X-Query-Group-Id', // mainly useful to find related queries with query splitting FromExpression = 'X-Grafana-From-Expr', // used by datasources to identify expression queries SkipQueryCache = 'X-Cache-Skip', // used by datasources to skip the query cache + DashboardTitle = 'X-Dashboard-Title', // used by datasources to identify the dashboard title + PanelTitle = 'X-Panel-Title', // used by datasources to identify the panel title } /** @@ -242,9 +244,15 @@ class DataSourceWithBackend< if (request.dashboardUID) { headers[PluginRequestHeaders.DashboardUID] = request.dashboardUID; + if (request.dashboardTitle) { + headers[PluginRequestHeaders.DashboardTitle] = request.dashboardTitle; + } if (request.panelId) { headers[PluginRequestHeaders.PanelID] = `${request.panelId}`; } + if (request.panelName) { + headers[PluginRequestHeaders.PanelTitle] = request.panelName; + } } if (request.panelPluginId) { headers[PluginRequestHeaders.PanelPluginId] = `${request.panelPluginId}`; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 8bf781a739d..214a47abc27 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -625,6 +625,7 @@ describe('DashboardScene', () => { expect(scene.enrichDataRequest(queryRunner)).toEqual({ app: CoreApp.Dashboard, dashboardUID: 'dash-1', + dashboardTitle: 'hello', panelId: 1, panelName: 'Panel A', panelPluginId: 'table', @@ -641,6 +642,7 @@ describe('DashboardScene', () => { expect(scene.enrichDataRequest(queryRunner)).toEqual({ app: CoreApp.Dashboard, dashboardUID: 'dash-1', + dashboardTitle: 'hello', panelId: 1, panelName: 'Panel A', panelPluginId: 'table', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index b0615c4ec69..cd54c64a63f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -682,6 +682,7 @@ export class DashboardScene extends SceneObjectBase impleme panelId, panelName: panel?.state?.title, panelPluginId: panel?.state.pluginId, + dashboardTitle: this.state.title, }; } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx index a8cc437dd5c..85f37be782c 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.test.tsx @@ -146,6 +146,7 @@ describe('PanelEditorTableView', () => { // panel queries should have the updated time range expect(props.panel.runAllPanelQueries).toHaveBeenNthCalledWith(1, { dashboardTimezone: '', + dashboardTitle: 'No Title', dashboardUID: props.dashboard.uid, timeData: timeRangeUpdated, width: 100, @@ -166,6 +167,7 @@ describe('PanelEditorTableView', () => { // panel queries should have the updated time range expect(props.panel.runAllPanelQueries).toHaveBeenLastCalledWith({ dashboardTimezone: '', + dashboardTitle: 'No Title', dashboardUID: props.dashboard.uid, timeData: timeRangeUpdated2, width: 100, diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx index 1855e7fa993..be11ac1bfdd 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTableView.tsx @@ -38,6 +38,7 @@ export function PanelEditorTableView({ width, height, panel, dashboard }: Props) panel.runAllPanelQueries({ dashboardUID: dashboard.uid, dashboardTimezone: dashboard.getTimezone(), + dashboardTitle: dashboard.title, timeData, width, }); diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index 4f3d12cd7d6..e088ab37f93 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -362,6 +362,7 @@ export class PanelStateWrapper extends PureComponent { panel.runAllPanelQueries({ dashboardUID: dashboard.uid, dashboardTimezone: dashboard.getTimezone(), + dashboardTitle: dashboard.title, timeData, width, }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 5d16a8cce74..3002dd32134 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -49,6 +49,7 @@ export interface GridPos { type RunPanelQueryOptions = { dashboardUID: string; dashboardTimezone: string; + dashboardTitle: string; timeData: TimeOverrideResult; width: number; publicDashboardAccessToken?: string; @@ -363,7 +364,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { this.render(); } - runAllPanelQueries({ dashboardUID, dashboardTimezone, timeData, width }: RunPanelQueryOptions) { + runAllPanelQueries({ dashboardUID, dashboardTimezone, timeData, width, dashboardTitle }: RunPanelQueryOptions) { this.getQueryRunner().run({ datasource: this.datasource, queries: this.targets, @@ -371,6 +372,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { panelName: this.title, panelPluginId: this.type, dashboardUID: dashboardUID, + dashboardTitle: dashboardTitle, timezone: dashboardTimezone, timeRange: timeData.timeRange, timeInfo: timeData.timeInfo, diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index 9a4f70c0b99..375cd4d9c1f 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -53,6 +53,7 @@ export interface QueryRunnerOptions< panelName?: string; panelPluginId?: string; dashboardUID?: string; + dashboardTitle?: string; timezone: TimeZone; timeRange: TimeRange; timeInfo?: string; // String description of time range for display @@ -262,6 +263,7 @@ export class PanelQueryRunner { panelName, panelPluginId, dashboardUID, + dashboardTitle, timeRange, timeInfo, cacheTimeout, @@ -288,6 +290,7 @@ export class PanelQueryRunner { panelName, panelPluginId, dashboardUID, + dashboardTitle, range: timeRange, timeInfo, interval: '', diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 7b2e399bdfd..7c98dbf6dd8 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -31,7 +31,6 @@ import { setBackendSrv, TemplateSrv, } from '@grafana/runtime'; -import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { LokiVariableSupport } from './LokiVariableSupport'; import { createLokiDatasource } from './__mocks__/datasource'; @@ -1810,41 +1809,6 @@ describe('LokiDatasource', () => { }); }); - describe('query', () => { - beforeEach(() => { - setDashboardSrv({ - getCurrent: () => ({ - title: 'dashboard_title', - panels: [{ title: 'panel_title', id: 0 }], - }), - } as unknown as DashboardSrv); - const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); - setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); - }); - - it('adds dashboard headers', async () => { - const ds = createLokiDatasource(templateSrvStub); - jest.spyOn(ds, 'runQuery'); - const query: DataQueryRequest = { - ...baseRequestOptions, - panelId: 0, - targets: [{ expr: '{a="b"}', refId: 'A' }], - app: CoreApp.Dashboard, - }; - - await expect(ds.query(query)).toEmitValuesWith(() => { - expect(ds.runQuery).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Dashboard-Title': 'dashboard_title', - 'X-Panel-Title': 'panel_title', - }), - }) - ); - }); - }); - }); - describe('getQueryStats', () => { let ds: LokiDatasource; let query: LokiQuery; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 7074ece8325..830c8ebb70e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -46,7 +46,6 @@ import { import { Duration } from '@grafana/lezer-logql'; import { BackendSrvRequest, config, DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; -import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import LanguageProvider from './LanguageProvider'; import { LiveStreams, LokiLiveTarget } from './LiveStreams'; @@ -295,30 +294,6 @@ export class LokiDatasource return { ...logsSampleRequest, targets }; } - private getQueryHeaders(request: DataQueryRequest): Record { - const headers: Record = {}; - // only add headers if we are in the context of a dashboard - if ( - [CoreApp.Dashboard.toString(), CoreApp.PanelEditor.toString(), CoreApp.PanelViewer.toString()].includes( - request.app - ) === false - ) { - return headers; - } - - const dashboard = getDashboardSrv().getCurrent(); - const dashboardTitle = dashboard?.title; - const panelTitle = dashboard?.panels.find((p) => p.id === request?.panelId)?.title; - if (dashboardTitle) { - headers['X-Dashboard-Title'] = dashboardTitle; - } - if (panelTitle) { - headers['X-Panel-Title'] = panelTitle; - } - - return headers; - } - /** * Required by DataSourceApi. It executes queries based on the provided DataQueryRequest. * @returns An Observable of DataQueryResponse containing the query results. @@ -333,8 +308,6 @@ export class LokiDatasource targets: queries, }; - fixedRequest.headers = this.getQueryHeaders(request); - const streamQueries = fixedRequest.targets.filter((q) => q.queryType === LokiQueryType.Stream); if ( config.featureToggles.lokiExperimentalStreaming &&