Querying: Pass dashboard and panel title as headers (#107032)

* WIP dashboard titile

* Add tests

* Fix tests

* Fix tests

* Remove redundant imports
This commit is contained in:
Ivana Huckova 2025-06-25 13:51:23 +02:00 committed by GitHub
parent 3d1b820827
commit 4738957360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 75 additions and 64 deletions

View File

@ -574,6 +574,7 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
panelName?: string;
panelPluginId?: string;
dashboardUID?: string;
dashboardTitle?: string;
headers?: Record<string, string>;
/** Filters to dynamically apply to all queries */

View File

@ -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({

View File

@ -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}`;

View File

@ -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',

View File

@ -682,6 +682,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
panelId,
panelName: panel?.state?.title,
panelPluginId: panel?.state.pluginId,
dashboardTitle: this.state.title,
};
}

View File

@ -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,

View File

@ -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,
});

View File

@ -362,6 +362,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
panel.runAllPanelQueries({
dashboardUID: dashboard.uid,
dashboardTimezone: dashboard.getTimezone(),
dashboardTitle: dashboard.title,
timeData,
width,
});

View File

@ -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,

View File

@ -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: '',

View File

@ -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<LokiQuery> = {
...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;

View File

@ -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<LokiQuery>): Record<string, string> {
const headers: Record<string, string> = {};
// 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 &&