Dashboard Schema V2: Preserve ds refs that only have type (#114734)

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Haris Rozajac
2025-12-03 11:29:52 +00:00
committed by GitHub
co-authored by Ivan Ortega Dominik Prokop
parent 612af5ef55
commit f3ed3a999d
4 changed files with 130 additions and 63 deletions
@@ -1392,8 +1392,8 @@ describe('DashboardSceneSerializer', () => {
serializer.initializeDSReferencesMapping(v1SaveModel as unknown as DashboardV2Spec);
expect(serializer.getDSReferencesMapping()).toEqual({
panels: new Map(),
variables: new Set(),
annotations: new Set(),
variables: new Map(),
annotations: new Map(),
});
expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
});
@@ -1410,8 +1410,8 @@ describe('DashboardSceneSerializer', () => {
serializer.initializeDSReferencesMapping(undefined);
expect(serializer.getDSReferencesMapping()).toEqual({
panels: expect.any(Map),
variables: expect.any(Set),
annotations: expect.any(Set),
variables: expect.any(Map),
annotations: expect.any(Map),
});
expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
});
@@ -99,9 +99,12 @@ interface DynamicDashboardsTrackingInformationLayoutParsing
}
export interface DSReferencesMapping {
panels: Map<string, Set<string>>;
variables: Set<string>;
annotations: Set<string>;
// panel id as keys, map as value. Map<refId, group> as value, if undefined, it means the datasource type was not defined
panels: Map<string, Map<string, string | undefined>>;
// variable name as keys, group as value, if undefined, it means the datasource type was not defined
variables: Map<string, string | undefined>;
// annotation name as keys, group as value, if undefined, it means the datasource type was not defined
annotations: Map<string, string | undefined>;
}
export class V1DashboardSerializer
@@ -110,10 +113,10 @@ export class V1DashboardSerializer
initialSaveModel?: Dashboard;
metadata?: DashboardMeta;
protected elementPanelMap = new Map<string, number>();
protected defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(), // refIds as keys
variables: new Set<string>(), // variable names as keys
annotations: new Set<string>(), // annotation names as keys
protected defaultDsReferencesMap: DSReferencesMapping = {
panels: new Map(),
variables: new Map(),
annotations: new Map(),
};
initializeElementMapping(saveModel: Dashboard | undefined) {
@@ -274,10 +277,10 @@ export class V2DashboardSerializer
metadata?: DashboardWithAccessInfo<DashboardV2Spec>['metadata'];
protected elementPanelMap = new Map<string, number>();
// map of elementId that will contain all the queries, variables and annotations that dont have a ds defined
protected defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(), // refIds as keys
variables: new Set<string>(), // variable names as keys
annotations: new Set<string>(), // annotation names as keys
protected defaultDsReferencesMap: DSReferencesMapping = {
panels: new Map(),
variables: new Map(),
annotations: new Map(),
};
getElementPanelMapping() {
@@ -308,13 +311,9 @@ export class V2DashboardSerializer
return;
}
// initialize the object
this.defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(),
variables: new Set<string>(),
annotations: new Set<string>(),
};
this.defaultDsReferencesMap = { panels: new Map(), variables: new Map(), annotations: new Map() };
// get all the element keys
// initialize autossigned panel queries ds references map
const elementKeys = Object.keys(saveModel?.elements || {});
elementKeys.forEach((key) => {
const elementPanel = saveModel?.elements[key];
@@ -324,14 +323,13 @@ export class V2DashboardSerializer
for (const query of panelQueries) {
if (!query.spec.query.datasource?.name) {
// Datasources without UID. Here we're saving elements with only type!
const elementId = this.getElementIdForPanel(elementPanel.spec.id);
if (!this.defaultDsReferencesMap.panels.has(elementId)) {
this.defaultDsReferencesMap.panels.set(elementId, new Set());
}
const panelDsqueries = this.defaultDsReferencesMap.panels.get(elementId) || new Map();
const datasourceType = query.spec.query.group || undefined;
const panelDsqueries = this.defaultDsReferencesMap.panels.get(elementId)!;
panelDsqueries.add(query.spec.refId);
panelDsqueries.set(query.spec.refId, datasourceType);
this.defaultDsReferencesMap.panels.set(elementId, panelDsqueries);
}
}
}
@@ -342,7 +340,8 @@ export class V2DashboardSerializer
for (const variable of saveModel.variables) {
// for query variables that dont have a ds defined add them to the list
if (variable.kind === 'QueryVariable' && !variable.spec.query.datasource?.name) {
this.defaultDsReferencesMap.variables.add(variable.spec.name);
const datasourceType = variable.spec.query.group || undefined;
this.defaultDsReferencesMap.variables.set(variable.spec.name, datasourceType);
}
}
}
@@ -351,7 +350,8 @@ export class V2DashboardSerializer
if (saveModel?.annotations) {
for (const annotation of saveModel.annotations) {
if (!annotation.spec.query?.datasource?.name) {
this.defaultDsReferencesMap.annotations.add(annotation.spec.name);
const datasourceType = annotation.spec.query.group || undefined;
this.defaultDsReferencesMap.annotations.set(annotation.spec.name, datasourceType);
}
}
}
@@ -470,6 +470,11 @@ describe('transformSceneToSaveModelSchemaV2', () => {
datasource: { uid: 'prometheus', type: 'prometheus' },
};
const queryWithOnlyDSType: SceneDataQuery = {
refId: 'C',
datasource: { type: 'prometheus' },
};
// Mock query runner with runtime-resolved datasource
const queryRunner = new SceneQueryRunner({
queries: [queryWithoutDS, queryWithDS],
@@ -477,7 +482,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
});
// Get a reference to the DS references mapping
const dsReferencesMap = new Set(['A']);
const dsReferencesMap = new Map<string, string | undefined>([['A', undefined]]);
// Test the query without DS originally - should return undefined
const resultA = getPersistedDSFor(queryWithoutDS, dsReferencesMap, 'query', queryRunner);
@@ -487,13 +492,17 @@ describe('transformSceneToSaveModelSchemaV2', () => {
const resultB = getPersistedDSFor(queryWithDS, dsReferencesMap, 'query', queryRunner);
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
// Test the query with only type defined - should return the type
const resultC = getPersistedDSFor(queryWithOnlyDSType, dsReferencesMap, 'query', queryRunner);
expect(resultC).toEqual({ type: 'prometheus' });
// Test a query with no DS originally but not in the mapping - should get the runner's datasource
const queryNotInMapping: SceneDataQuery = {
refId: 'C',
refId: 'D',
// No datasource, but not in mapping
};
const resultC = getPersistedDSFor(queryNotInMapping, dsReferencesMap, 'query', queryRunner);
expect(resultC).toEqual({ uid: 'default-ds', type: 'default' });
const resultD = getPersistedDSFor(queryNotInMapping, dsReferencesMap, 'query', queryRunner);
expect(resultD).toEqual({ uid: 'default-ds', type: 'default' });
});
});
@@ -511,8 +520,14 @@ describe('transformSceneToSaveModelSchemaV2', () => {
datasource: { uid: 'prometheus', type: 'prometheus' },
});
// Variable with only type defined
const variableWithOnlyDSType = new QueryVariable({
name: 'C',
datasource: { type: 'prometheus' },
});
// Get a reference to the DS references mapping
const dsReferencesMap = new Set(['A']);
const dsReferencesMap = new Map<string, string | undefined>([['A', undefined]]);
// Test the variable without DS originally - should return undefined
const resultA = getPersistedDSFor(variableWithoutDS, dsReferencesMap, 'variable');
@@ -522,13 +537,17 @@ describe('transformSceneToSaveModelSchemaV2', () => {
const resultB = getPersistedDSFor(variableWithDS, dsReferencesMap, 'variable');
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
// Test a variable with no DS originally but not in the mapping - should get empty object
// Test the variable with only type defined - should return the type
const resultC = getPersistedDSFor(variableWithOnlyDSType, dsReferencesMap, 'variable');
expect(resultC).toEqual({ type: 'prometheus' });
// Test a variable with no DS originally but not in the mapping - should return undefined
const variableNotInMapping = new QueryVariable({
name: 'C',
name: 'D',
// No datasource, but not in mapping
});
const resultC = getPersistedDSFor(variableNotInMapping, dsReferencesMap, 'variable');
expect(resultC).toEqual({});
const resultD = getPersistedDSFor(variableNotInMapping, dsReferencesMap, 'variable');
expect(resultD).toBeUndefined();
});
});
@@ -647,6 +666,11 @@ describe('getElementDatasource', () => {
refId: 'A',
};
const queryWithOnlyType: SceneDataQuery = {
refId: 'C',
datasource: { type: 'prometheus' },
};
// Mock query runner
const queryRunner = new SceneQueryRunner({
queries: [queryWithoutDS, queryWithDS],
@@ -655,9 +679,9 @@ describe('getElementDatasource', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
// Call the function with the panel and query with DS
@@ -667,6 +691,16 @@ describe('getElementDatasource', () => {
// Call the function with the panel and query without DS
const resultWithoutDS = getElementDatasource(vizPanel, queryWithoutDS, 'panel', queryRunner, dsReferencesMapping);
expect(resultWithoutDS).toBeUndefined();
// Call the function with the panel and query with only type
const resultWithOnlyType = getElementDatasource(
vizPanel,
queryWithOnlyType,
'panel',
queryRunner,
dsReferencesMapping
);
expect(resultWithOnlyType).toEqual({ type: 'prometheus' });
});
it('should handle variable datasources correctly', () => {
@@ -692,9 +726,9 @@ describe('getElementDatasource', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(['A']),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>([['A', '']]),
annotations: new Map<string, string>(),
};
// Call the function with variables
@@ -781,11 +815,25 @@ describe('getElementDatasource', () => {
iconColor: 'blue',
};
// Create an annotation query with only type defined
const annotationLayerWithOnlyType = new dataLayers.AnnotationsDataLayer({
name: 'Annotation with only datasource type',
isEnabled: true,
isHidden: false,
query: {
name: 'Test Annotation',
enable: true,
hide: false,
iconColor: 'blue',
datasource: { type: 'prometheus' },
},
});
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map([['panel-1', new Set(['A'])]]),
variables: new Set<string>(),
annotations: new Set<string>(['No DS Annotation']),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
// Test with annotation that has datasource defined
@@ -807,6 +855,16 @@ describe('getElementDatasource', () => {
dsReferencesMapping
);
expect(resultWithoutDS).toBeUndefined();
// Test with annotation that has only type defined
const resultWithOnlyType = getElementDatasource(
annotationLayer,
annotationLayerWithOnlyType.state.query,
'annotation',
undefined,
dsReferencesMapping
);
expect(resultWithOnlyType).toEqual({ type: 'prometheus' });
});
it('should handle invalid input combinations', () => {
@@ -878,9 +936,9 @@ describe('getVizPanelQueries', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
const result = getVizPanelQueries(vizPanel, dsReferencesMapping);
@@ -808,13 +808,13 @@ export function getAutoAssignedDSRef(
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
type: 'panels' | 'variables' | 'annotations',
elementMapReferences?: DSReferencesMapping
): Set<string> {
): Map<string, string | undefined> {
if (!elementMapReferences) {
return new Set();
return new Map();
}
if (type === 'panels' && isVizPanel(element)) {
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(element);
return elementMapReferences.panels.get(elementKey) || new Set();
return elementMapReferences.panels.get(elementKey) || new Map();
}
if (type === 'variables') {
@@ -830,20 +830,26 @@ export function getAutoAssignedDSRef(
}
/**
* Determines if a data source reference should be persisted for a query or variable
* Returns the datasource value that should be persisted for a panel query, variable or annotation
* - Undefined if the datasource was not defined in the initial save model
* - { type: string } if the datasource was autossigned defined by the initial group value
* - { uid: string, type: string } if the datasource was defined in the initial save model
*/
export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
element: T,
autoAssignedDsRef: Set<string>,
autoAssignedDsRef: Map<string, string | undefined>,
type: 'query' | 'variable' | 'annotation',
context?: SceneQueryRunner
): DataSourceRef | undefined {
// Get the element identifier - refId for queries, name for variables
const elementId = getElementIdentifier(element, type);
// If the element is in the auto-assigned set, it didn't have a datasource specified
// If the ds was autossigned, return the datasource initial ds value.
if (autoAssignedDsRef?.has(elementId)) {
return undefined;
const dsType = autoAssignedDsRef.get(elementId);
// If the ds type was not undefined means the datasource was autossigned, so return the datasource with only the type
return dsType ? { type: dsType } : undefined;
}
// Return appropriate datasource reference based on element type
@@ -858,11 +864,11 @@ export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | Ann
}
if (type === 'variable' && 'state' in element && 'datasource' in element.state) {
return element.state.datasource || {};
return element.state.datasource || undefined;
}
if (type === 'annotation' && 'datasource' in element) {
return element.datasource || {};
return element.datasource || undefined;
}
return undefined;
@@ -920,11 +926,14 @@ function isQueryVariable(query: SceneDataQuery | QueryVariable | AnnotationQuery
}
/**
* Get the persisted datasource for a query or variable
* When a query or variable is created it could not have a datasource set
* Get the persisted datasource for a panel query, annotation or variable
* When a panel query, annotation or variable is created it could not have a datasource set
* we want to respect that and not overwrite it with the auto assigned datasources
* resolved in runtime
*
* - Undefined if the datasource was not defined in the initial save model
* - { type: string } if the datasource was autossigned defined by the initial group value
* - { uid: string, type: string } if the datasource was defined in the initial save model
*/
export function getElementDatasource(
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
@@ -961,7 +970,7 @@ export function getElementDatasource(
const autoAssignedRefs = getAutoAssignedDSRef(element, 'annotations', dsReferencesMapping);
result = getPersistedDSFor(queryElement, autoAssignedRefs, 'annotation');
}
// Important: Only return the datasource if it's not in auto-assigned refs
// and if the result would not be an empty object
// Avoid returning an empty object
return Object.keys(result || {}).length > 0 ? result : undefined;
}