From 98cbecc4a5cc05bd1e51e66ddc0014251b0dd7fe Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 12 Apr 2022 13:52:55 +0200 Subject: [PATCH] Allow queries import when changing data source type (#47435) * Enable queries import when changing datasource * Supporting empty imports * Review applied --- .../features/query/components/QueryGroup.tsx | 5 +- .../query/state/updateQueries.test.ts | 348 +++++++++++++++--- .../app/features/query/state/updateQueries.ts | 39 +- 3 files changed, 326 insertions(+), 66 deletions(-) diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 438d0b4c207..a28f71cc65d 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -111,7 +111,10 @@ export class QueryGroup extends PureComponent { onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => { const { dsSettings } = this.state; - const queries = updateQueries(newSettings, this.state.queries, dsSettings); + const currentDS = dsSettings ? await getDataSourceSrv().get(dsSettings.uid) : undefined; + const nextDS = await getDataSourceSrv().get(newSettings.uid); + + const queries = await updateQueries(nextDS, this.state.queries, currentDS); const dataSource = await this.dataSourceSrv.get(newSettings.name); this.onChange({ diff --git a/public/app/features/query/state/updateQueries.test.ts b/public/app/features/query/state/updateQueries.test.ts index 13dc5b4ad56..f4f8a2de817 100644 --- a/public/app/features/query/state/updateQueries.test.ts +++ b/public/app/features/query/state/updateQueries.test.ts @@ -1,20 +1,54 @@ +import { + DataQuery, + DataSourceApi, + DataSourceWithQueryExportSupport, + DataSourceWithQueryImportSupport, +} from '@grafana/data'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { updateQueries } from './updateQueries'; +const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, +} as DataSourceApi; + +const mixedDS = { + uid: 'mixed', + meta: { + id: 'mixed', + mixed: true, + }, +} as DataSourceApi; + +const newUidDS = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, +} as DataSourceApi; + +const newUidSameTypeDS = { + uid: 'new-uid-same-type', + type: 'old-type', + meta: { + id: 'old-type', + }, +} as DataSourceApi; + describe('updateQueries', () => { - it('Should update all queries except expression query when changing data source with same type', () => { - const updated = updateQueries( - { - uid: 'new-uid', - type: 'same-type', - meta: {}, - } as any, + it('Should update all queries except expression query when changing data source with same type', async () => { + const updated = await updateQueries( + newUidSameTypeDS, [ { refId: 'A', datasource: { uid: 'old-uid', - type: 'same-type', + type: 'old-type', }, }, { @@ -22,23 +56,16 @@ describe('updateQueries', () => { datasource: ExpressionDatasourceRef, }, ], - { - uid: 'old-uid', - type: 'same-type', - } as any + oldUidDS ); - expect(updated[0].datasource).toEqual({ type: 'same-type', uid: 'new-uid' }); + expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'new-uid-same-type' }); expect(updated[1].datasource).toEqual(ExpressionDatasourceRef); }); - it('Should clear queries when changing type', () => { - const updated = updateQueries( - { - uid: 'new-uid', - type: 'new-type', - meta: {}, - } as any, + it('Should clear queries when changing type', async () => { + const updated = await updateQueries( + newUidDS, [ { refId: 'A', @@ -55,25 +82,16 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'old-uid', - type: 'old-type', - } as any + oldUidDS ); expect(updated.length).toEqual(1); expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); }); - it('Should preserve query data source when changing to mixed', () => { - const updated = updateQueries( - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any, + it('Should preserve query data source when changing to mixed', async () => { + const updated = await updateQueries( + mixedDS, [ { refId: 'A', @@ -90,25 +108,16 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'old-uid', - type: 'old-type', - } as any + oldUidDS ); expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'old-uid' }); expect(updated[1].datasource).toEqual({ type: 'other-type', uid: 'other-uid' }); }); - it('should change nothing mixed updated to mixed', () => { - const updated = updateQueries( - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any, + it('should change nothing mixed updated to mixed', async () => { + const updated = await updateQueries( + mixedDS, [ { refId: 'A', @@ -125,16 +134,249 @@ describe('updateQueries', () => { }, }, ], - { - uid: 'mixed', - type: 'mixed', - meta: { - mixed: true, - }, - } as any + mixedDS ); expect(updated[0].datasource).toEqual({ type: 'old-type', uid: 'old-uid' }); expect(updated[1].datasource).toEqual({ type: 'other-type', uid: 'other-uid' }); }); }); + +describe('updateQueries with import', () => { + describe('abstract queries support', () => { + it('should migrate abstract queries', async () => { + const exportSpy = jest.fn(); + const importSpy = jest.fn(); + + const newUidDSWithAbstract = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importFromAbstractQueries: (queries) => { + importSpy(queries); + const importedQueries = queries.map((q) => ({ ...q, imported: true })); + return Promise.resolve(importedQueries); + }, + } as DataSourceWithQueryImportSupport; + + const oldUidDSWithAbstract = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + exportToAbstractQueries: (queries) => { + exportSpy(queries); + const exportedQueries = queries.map((q) => ({ ...q, exported: true })); + return Promise.resolve(exportedQueries); + }, + } as DataSourceWithQueryExportSupport; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithAbstract as any, queries, oldUidDSWithAbstract as any); + + expect(exportSpy).toBeCalledWith(queries); + expect(importSpy).toBeCalledWith(queries.map((q) => ({ ...q, exported: true }))); + + expect(updated).toMatchInlineSnapshot(` + Array [ + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "exported": true, + "imported": true, + "refId": "A", + }, + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "exported": true, + "imported": true, + "refId": "B", + }, + ] + `); + }); + + it('should clear queries when no queries were imported', async () => { + const newUidDSWithAbstract = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importFromAbstractQueries: () => { + return Promise.resolve([]); + }, + } as DataSourceWithQueryImportSupport; + + const oldUidDSWithAbstract = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + exportToAbstractQueries: (queries) => { + const exportedQueries = queries.map((q) => ({ ...q, exported: true })); + return Promise.resolve(exportedQueries); + }, + } as DataSourceWithQueryExportSupport; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithAbstract as any, queries, oldUidDSWithAbstract as any); + + expect(updated.length).toEqual(1); + expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); + }); + }); + + describe('importQueries support', () => { + it('should import queries when abstract queries are not supported by datasources', async () => { + const importSpy = jest.fn(); + + const newUidDSWithImport = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importQueries: (queries, origin) => { + importSpy(queries, origin); + const importedQueries = queries.map((q) => ({ ...q, imported: true })); + return Promise.resolve(importedQueries); + }, + } as DataSourceApi; + + const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + } as DataSourceApi; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithImport, queries, oldUidDS); + + expect(importSpy).toBeCalledWith(queries, { uid: 'old-uid', type: 'old-type', meta: { id: 'old-type' } }); + + expect(updated).toMatchInlineSnapshot(` + Array [ + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "imported": true, + "refId": "A", + }, + Object { + "datasource": Object { + "type": "new-type", + "uid": "new-uid", + }, + "imported": true, + "refId": "B", + }, + ] + `); + }); + + it('should clear queries when no queries were imported', async () => { + const newUidDSWithImport = { + uid: 'new-uid', + type: 'new-type', + meta: { + id: 'new-type', + }, + importQueries: (queries, origin) => { + return Promise.resolve([] as DataQuery[]); + }, + } as DataSourceApi; + + const oldUidDS = { + uid: 'old-uid', + type: 'old-type', + meta: { + id: 'old-type', + }, + } as DataSourceApi; + + const queries = [ + { + refId: 'A', + datasource: { + uid: 'old-uid', + type: 'old-type', + }, + }, + { + refId: 'B', + datasource: { + uid: 'other-uid', + type: 'other-type', + }, + }, + ]; + + const updated = await updateQueries(newUidDSWithImport, queries, oldUidDS); + + expect(updated.length).toEqual(1); + expect(updated[0].datasource).toEqual({ type: 'new-type', uid: 'new-uid' }); + }); + }); +}); diff --git a/public/app/features/query/state/updateQueries.ts b/public/app/features/query/state/updateQueries.ts index d6998173599..3eb1a1b5aa7 100644 --- a/public/app/features/query/state/updateQueries.ts +++ b/public/app/features/query/state/updateQueries.ts @@ -1,27 +1,42 @@ -import { DataQuery, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; +import { DataQuery, DataSourceApi, hasQueryExportSupport, hasQueryImportSupport } from '@grafana/data'; import { isExpressionReference } from '@grafana/runtime/src/utils/DataSourceWithBackend'; -export function updateQueries( - newSettings: DataSourceInstanceSettings, +export async function updateQueries( + nextDS: DataSourceApi, queries: DataQuery[], - dsSettings?: DataSourceInstanceSettings -): DataQuery[] { - const datasource = getDataSourceRef(newSettings); + currentDS?: DataSourceApi +): Promise { + let nextQueries = queries; + const datasource = { type: nextDS.type, uid: nextDS.uid }; // we are changing data source type - if (dsSettings?.type !== newSettings.type) { + if (currentDS?.meta.id !== nextDS.meta.id) { // If changing to mixed do nothing - if (newSettings.meta.mixed) { + if (nextDS.meta.mixed) { return queries; - } else { - // Changing to another datasource type clear queries + } + // when both data sources support abstract queries + else if (hasQueryExportSupport(currentDS) && hasQueryImportSupport(nextDS)) { + const abstractQueries = await currentDS.exportToAbstractQueries(queries); + nextQueries = await nextDS.importFromAbstractQueries(abstractQueries); + } + // when datasource supports query import + else if (currentDS && nextDS.importQueries) { + nextQueries = await nextDS.importQueries(queries, currentDS); + } + // Otherwise clear queries + else { return [{ refId: 'A', datasource }]; } } + if (nextQueries.length === 0) { + return [{ refId: 'A', datasource }]; + } + // Set data source on all queries except expression queries - return queries.map((query) => { - if (!isExpressionReference(query.datasource) && !newSettings.meta.mixed) { + return nextQueries.map((query) => { + if (!isExpressionReference(query.datasource) && !nextDS.meta.mixed) { query.datasource = datasource; } return query;