From 195bf681d14c379cd113a87b71456e7df9581e2f Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 16 Dec 2025 13:34:08 +0100 Subject: [PATCH] V2: Ensure refIds for queries (#115404) Ensure refIds for queries --- .../layoutSerializers/utils.test.ts | 75 ++++++++++++++++++- .../serialization/layoutSerializers/utils.ts | 21 +++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts index 328dc680e6e..ad644f92294 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts @@ -1,6 +1,6 @@ import { defaultDataQueryKind, PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; -import { getRuntimePanelDataSource } from './utils'; +import { ensureUniqueRefIds, getRuntimePanelDataSource } from './utils'; describe('getRuntimePanelDataSource', () => { it('should return uid and type when explicit datasource UID is provided', () => { @@ -140,3 +140,76 @@ describe('getRuntimePanelDataSource', () => { expect(result).toBeUndefined(); }); }); + +describe('ensureUniqueRefIds', () => { + const createQuery = (refId: string): PanelQueryKind => ({ + kind: 'PanelQuery', + spec: { + refId, + hidden: false, + query: { + kind: 'DataQuery', + version: defaultDataQueryKind().version, + group: 'prometheus', + spec: {}, + }, + }, + }); + + it('should assign unique refIds to queries without refIds', () => { + const queries: PanelQueryKind[] = [createQuery(''), createQuery(''), createQuery('')]; + + const result = ensureUniqueRefIds(queries); + + expect(result[0].spec.refId).toBe('A'); + expect(result[1].spec.refId).toBe('B'); + expect(result[2].spec.refId).toBe('C'); + }); + + it('should preserve existing refIds and fill gaps', () => { + const queries: PanelQueryKind[] = [createQuery('A'), createQuery(''), createQuery('D'), createQuery('')]; + + const result = ensureUniqueRefIds(queries); + + expect(result[0].spec.refId).toBe('A'); + expect(result[1].spec.refId).toBe('B'); + expect(result[2].spec.refId).toBe('D'); + expect(result[3].spec.refId).toBe('C'); + }); + + it('should handle all queries having existing refIds', () => { + const queries: PanelQueryKind[] = [createQuery('A'), createQuery('B'), createQuery('C')]; + + const result = ensureUniqueRefIds(queries); + + expect(result[0].spec.refId).toBe('A'); + expect(result[1].spec.refId).toBe('B'); + expect(result[2].spec.refId).toBe('C'); + }); + + it('should only modify queries without refIds', () => { + const queries: PanelQueryKind[] = [createQuery('A'), createQuery(''), createQuery('C')]; + + const result = ensureUniqueRefIds(queries); + + // Existing refIds should be preserved + expect(result[0].spec.refId).toBe('A'); + expect(result[2].spec.refId).toBe('C'); + // Missing refId should be assigned + expect(result[1].spec.refId).toBe('B'); + }); + + it('should handle empty array', () => { + const result = ensureUniqueRefIds([]); + + expect(result).toEqual([]); + }); + + it('should handle single query without refId', () => { + const queries: PanelQueryKind[] = [createQuery('')]; + + const result = ensureUniqueRefIds(queries); + + expect(result[0].spec.refId).toBe('A'); + }); +}); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index fc634339607..6595268025d 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -1,3 +1,4 @@ +import { getNextRefId } from '@grafana/data'; import { config } from '@grafana/runtime'; import { SceneDataProvider, @@ -164,12 +165,15 @@ export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider return undefined; } + // Ensure all queries have unique refIds before converting to scene queries + const queriesWithUniqueRefIds = ensureUniqueRefIds(targets); + let dataProvider: SceneDataProvider | undefined = undefined; const datasource = getPanelDataSource(panelKind); dataProvider = new SceneQueryRunner({ datasource, - queries: targets.map(panelQueryKindToSceneQuery), + queries: queriesWithUniqueRefIds.map(panelQueryKindToSceneQuery), maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined, maxDataPointsFromWidth: true, cacheTimeout: panel.data.spec.queryOptions.cacheTimeout, @@ -341,6 +345,21 @@ export function getDataSourceForQuery(querySpecDS: DataSourceRef | undefined | n }; } +export function ensureUniqueRefIds(queries: PanelQueryKind[]): PanelQueryKind[] { + // Adapter to make PanelQueryKind[] work with getNextRefId (which expects { refId }[]) + const refIdAdapter = queries.map((q) => ({ refId: q.spec.refId })); + + for (let i = 0; i < queries.length; i++) { + if (!queries[i].spec.refId) { + const newRefId = getNextRefId(refIdAdapter); + queries[i] = { ...queries[i], spec: { ...queries[i].spec, refId: newRefId } }; + refIdAdapter[i] = { refId: newRefId }; + } + } + + return queries; +} + function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { // Add datasource to match Go backend V2→V1 conversion: // - If explicit UID (datasource.name) exists → add { uid, type }