Dashboard: SchemaV2 Fix Import mapping datasource (#104200)

* Dashboard: SchemaV2 Fix mapping ds for variables and annotations

* process annotations and vars ds inputs so they can be selected

* clean up so async works

* Add basic unit test for v2 dashboards mapping

* clean up tests

* linting

---------

Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
Alexa V
2025-04-22 14:47:10 +02:00
committed by GitHub
parent fab0bdd634
commit 94e24f44b9
3 changed files with 267 additions and 39 deletions

View File

@ -39,8 +39,8 @@ export function ImportDashboardOverviewV2() {
...dashboard, ...dashboard,
title: form.dashboard.title, title: form.dashboard.title,
annotations: dashboard.annotations?.map((annotation: AnnotationQueryKind) => { annotations: dashboard.annotations?.map((annotation: AnnotationQueryKind) => {
if (annotation.spec.datasource?.type) { if (annotation.spec.query?.kind) {
const dsType = annotation.spec.datasource.type; const dsType = annotation.spec.query.kind;
if (form[`datasource-${dsType}` as keyof typeof form]) { if (form[`datasource-${dsType}` as keyof typeof form]) {
const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string };
return { return {
@ -59,8 +59,8 @@ export function ImportDashboardOverviewV2() {
}), }),
variables: dashboard.variables?.map((variable) => { variables: dashboard.variables?.map((variable) => {
if (variable.kind === 'QueryVariable') { if (variable.kind === 'QueryVariable') {
if (variable.spec.datasource?.type) { if (variable.spec.query?.kind) {
const dsType = variable.spec.datasource.type; const dsType = variable.spec.query.kind;
if (form[`datasource-${dsType}` as keyof typeof form]) { if (form[`datasource-${dsType}` as keyof typeof form]) {
const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string };
return { return {

View File

@ -2,6 +2,12 @@ import { thunkTester } from 'test/core/thunk/thunkTester';
import { DataSourceInstanceSettings, ThresholdsMode } from '@grafana/data'; import { DataSourceInstanceSettings, ThresholdsMode } from '@grafana/data';
import { defaultDashboard, FieldColorModeId } from '@grafana/schema'; import { defaultDashboard, FieldColorModeId } from '@grafana/schema';
import {
DashboardV2Spec,
defaultDashboardV2Spec,
defaultPanelSpec,
defaultQueryVariableSpec,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { getLibraryPanel } from 'app/features/library-panels/state/api';
@ -10,7 +16,7 @@ import { LibraryElementDTO } from '../../library-panels/types';
import { DashboardJson } from '../types'; import { DashboardJson } from '../types';
import { validateDashboardJson } from '../utils/validation'; import { validateDashboardJson } from '../utils/validation';
import { getLibraryPanelInputs, importDashboard, processDashboard } from './actions'; import { getLibraryPanelInputs, importDashboard, processDashboard, processV2Datasources } from './actions';
import { DataSourceInput, ImportDashboardDTO, initialImportDashboardState, InputType } from './reducers'; import { DataSourceInput, ImportDashboardDTO, initialImportDashboardState, InputType } from './reducers';
jest.mock('app/features/library-panels/state/api'); jest.mock('app/features/library-panels/state/api');
@ -18,6 +24,43 @@ const mocks = {
getLibraryPanel: jest.mocked(getLibraryPanel), getLibraryPanel: jest.mocked(getLibraryPanel),
}; };
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => ({
...jest.requireActual('@grafana/runtime').getDataSourceSrv(),
get: jest.fn().mockImplementation((dsType: { type: string }) => {
const dsList: {
[key: string]: {
uid: string;
name: string;
type: string;
meta: { id: string };
};
} = {
prometheus: {
uid: 'prom-uid',
name: 'prometheus',
type: 'prometheus',
meta: { id: 'prometheus' },
},
loki: {
uid: 'loki-uid',
name: 'Loki',
type: 'loki',
meta: { id: 'loki' },
},
grafana: {
uid: 'grafana-uid',
name: 'Grafana',
type: 'grafana',
meta: { id: 'grafana' },
},
};
return dsList[dsType.type];
}),
}),
}));
describe('importDashboard', () => { describe('importDashboard', () => {
it('Should send data source uid', async () => { it('Should send data source uid', async () => {
// note: the actual action returned is more complicated // note: the actual action returned is more complicated
@ -755,3 +798,164 @@ describe('processDashboard', () => {
expect(dsInputsForLibPanels).toHaveLength(0); expect(dsInputsForLibPanels).toHaveLength(0);
}); });
}); });
describe('processV2Datasources', () => {
let panels: DashboardV2Spec['elements'];
let v2DashboardJson: DashboardV2Spec;
beforeEach(() => {
panels = {
'element-panel-a': {
kind: 'Panel',
spec: {
...defaultPanelSpec(),
id: 1,
title: 'Panel A',
data: {
kind: 'QueryGroup',
spec: {
transformations: [],
queryOptions: {},
queries: [
{
kind: 'PanelQuery',
spec: {
refId: 'A',
hidden: false,
query: {
kind: 'prometheus',
spec: {
expr: 'access_evaluation_duration_count',
range: true,
},
},
},
},
],
},
},
},
},
};
v2DashboardJson = {
...defaultDashboardV2Spec(),
elements: {
...panels,
},
variables: [
{
kind: 'QueryVariable',
spec: {
...defaultQueryVariableSpec(),
name: 'var1',
query: {
kind: 'loki',
spec: {
expr: 'access_evaluation_duration_count',
range: true,
},
},
},
},
],
annotations: [
{
kind: 'AnnotationQuery',
spec: {
name: 'annotation1',
enable: true,
hide: false,
iconColor: 'red',
query: {
kind: 'loki',
spec: {
expr: 'access_evaluation_duration_count',
range: true,
},
},
},
},
],
layout: {
kind: 'GridLayout',
spec: {
items: [
{
kind: 'GridLayoutItem',
spec: {
x: 0,
y: 0,
width: 12,
height: 8,
element: {
kind: 'ElementReference',
name: 'element-panel-a',
},
},
},
{
kind: 'GridLayoutItem',
spec: {
x: 0,
y: 0,
width: 12,
height: 8,
element: {
kind: 'ElementReference',
name: 'element-panel-b',
},
},
},
],
},
},
};
});
// should set the correct inputs for panels
it('Should extract datasource inputs from panel queries, variables and annotations', async () => {
// Execute the test using thunkTester
const dispatchedActions = await thunkTester({
thunk: processV2Datasources,
initialState: {
inputs: [
// for panels
{
name: 'Prometheus',
pluginId: 'prometheus',
type: InputType.DataSource,
},
// for variables and annotations
{
name: 'Loki',
pluginId: 'loki',
type: InputType.DataSource,
},
],
},
})
.givenThunk(processV2Datasources)
.whenThunkIsDispatched(v2DashboardJson);
// Find the setInputs action in the dispatched actions
const setInputsAction = dispatchedActions.find((action) => action.type === 'manageDashboards/setInputs');
//
// Verify the action was dispatched
expect(setInputsAction).toBeDefined();
// Verify the datasource inputs were correctly extracted
expect(setInputsAction?.payload).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'prometheus',
pluginId: 'prometheus',
type: InputType.DataSource,
}),
expect.objectContaining({
name: 'Loki',
pluginId: 'loki',
type: InputType.DataSource,
}),
])
);
});
});

View File

@ -1,6 +1,11 @@
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime'; import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import {
Spec as DashboardV2Spec,
QueryVariableKind,
PanelQueryKind,
AnnotationQueryKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
@ -62,7 +67,7 @@ export function importDashboardJson(dashboard: any): ThunkResult<void> {
export function importDashboardV2Json(dashboard: DashboardV2Spec): ThunkResult<void> { export function importDashboardV2Json(dashboard: DashboardV2Spec): ThunkResult<void> {
return async (dispatch) => { return async (dispatch) => {
dispatch(setJsonDashboard(dashboard)); dispatch(setJsonDashboard(dashboard));
dispatch(processV2Elements(dashboard)); dispatch(processV2Datasources(dashboard));
}; };
} }
@ -152,9 +157,9 @@ function processElements(dashboardJson?: { __elements?: Record<string, LibraryEl
}; };
} }
function processV2Elements(dashboard: DashboardV2Spec): ThunkResult<void> { export function processV2Datasources(dashboard: DashboardV2Spec): ThunkResult<void> {
return async function (dispatch) { return async function (dispatch) {
const elements = dashboard.elements; const { elements, variables, annotations } = dashboard;
// get elements from dashboard // get elements from dashboard
// each element can only be a panel // each element can only be a panel
const inputs: Record<string, DataSourceInput> = {}; const inputs: Record<string, DataSourceInput> = {};
@ -162,39 +167,23 @@ function processV2Elements(dashboard: DashboardV2Spec): ThunkResult<void> {
if (element.kind !== 'Panel') { if (element.kind !== 'Panel') {
throw new Error('Only panels are currenlty supported in v2 dashboards'); throw new Error('Only panels are currenlty supported in v2 dashboards');
} }
if (element.spec.data.spec.queries.length > 0) {
for (const query of element.spec.data.spec.queries) { for (const query of element.spec.data.spec.queries) {
const datasourceRef = query.spec.datasource; await processV2DatasourceInput(query.spec, inputs);
if (!datasourceRef) {
let dataSourceInput: DataSourceInput | undefined;
const dsType = query.spec.query.kind;
const datasource = await getDatasourceSrv().get({ type: dsType });
if (!datasource) {
dataSourceInput = {
name: dsType,
label: dsType,
info: `No data sources of type ${dsType} found`,
value: '',
type: InputType.DataSource,
pluginId: dsType,
};
inputs[dsType] = dataSourceInput;
} else {
dataSourceInput = {
name: datasource.name,
label: datasource.name,
info: `Select a ${datasource.name} data source`,
value: datasource.uid,
type: InputType.DataSource,
pluginId: datasource.meta?.id,
};
inputs[datasource.meta?.id] = dataSourceInput;
}
} }
} }
} }
for (const variable of variables) {
if (variable.kind === 'QueryVariable') {
await processV2DatasourceInput(variable.spec, inputs);
}
}
for (const annotation of annotations) {
await processV2DatasourceInput(annotation.spec, inputs);
}
dispatch(setInputs(Object.values(inputs))); dispatch(setInputs(Object.values(inputs)));
}; };
} }
@ -340,3 +329,38 @@ export async function searchFolders(
export function getFolderByUid(uid: string): Promise<{ uid: string; title: string }> { export function getFolderByUid(uid: string): Promise<{ uid: string; title: string }> {
return getBackendSrv().get(`/api/folders/${uid}`); return getBackendSrv().get(`/api/folders/${uid}`);
} }
export async function processV2DatasourceInput(
obj: PanelQueryKind['spec'] | QueryVariableKind['spec'] | AnnotationQueryKind['spec'],
inputs: Record<string, DataSourceInput> = {}
) {
const datasourceRef = obj?.datasource;
if (!datasourceRef && obj?.query) {
const dsType = obj.query.kind;
const datasource = await getDatasourceSrv().get({ type: dsType });
let dataSourceInput: DataSourceInput | undefined;
if (datasource) {
dataSourceInput = {
name: datasource.name,
label: datasource.name,
info: `Select a ${datasource.name} data source`,
value: datasource.uid,
type: InputType.DataSource,
pluginId: datasource.meta?.id,
};
inputs[datasource.meta?.id] = dataSourceInput;
} else {
dataSourceInput = {
name: dsType,
label: dsType,
info: `No data sources of type ${dsType} found`,
value: '',
type: InputType.DataSource,
pluginId: dsType,
};
inputs[dsType] = dataSourceInput;
}
}
}