diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.test.ts b/public/app/plugins/datasource/mixed/MixedDataSource.test.ts index 4c18ce82f6e..a984e8dcbb8 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.test.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.test.ts @@ -1,9 +1,10 @@ -import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv'; +import { from } from 'rxjs'; import { getDataSourceSrv } from '@grafana/runtime'; +import { DataSourceInstanceSettings, LoadingState } from '@grafana/data'; + +import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv'; import { getQueryOptions } from 'test/helpers/getQueryOptions'; -import { LoadingState, DataSourceInstanceSettings } from '@grafana/data'; import { MixedDatasource } from './module'; -import { from } from 'rxjs'; const defaultDS = new MockDataSourceApi('DefaultDS', { data: ['DDD'] }); const datasourceSrv = new DatasourceSrvMock(defaultDS, { @@ -11,42 +12,89 @@ const datasourceSrv = new DatasourceSrvMock(defaultDS, { A: new MockDataSourceApi('DSA', { data: ['AAAA'] }), B: new MockDataSourceApi('DSB', { data: ['BBBB'] }), C: new MockDataSourceApi('DSC', { data: ['CCCC'] }), + D: new MockDataSourceApi('DSD', { data: [] }, {}, 'syntax error near FROM'), + E: new MockDataSourceApi('DSE', { data: [] }, {}, 'syntax error near WHERE'), }); jest.mock('@grafana/runtime', () => ({ + ...((jest.requireActual('@grafana/runtime') as unknown) as object), getDataSourceSrv: () => { return datasourceSrv; }, })); describe('MixedDatasource', () => { - const requestMixed = getQueryOptions({ - targets: [ - { refId: 'QA', datasource: 'A' }, // 1 - { refId: 'QB', datasource: 'B' }, // 2 - { refId: 'QC', datasource: 'C' }, // 3 - ], - }); - const results: any[] = []; + describe('with no errors', () => { + const requestMixed = getQueryOptions({ + targets: [ + { refId: 'QA', datasource: 'A' }, // 1 + { refId: 'QB', datasource: 'B' }, // 2 + { refId: 'QC', datasource: 'C' }, // 3 + ], + }); + const results: any[] = []; - beforeEach(async done => { - const ds = await getDataSourceSrv().get('-- Mixed --'); + beforeEach(async done => { + const ds = await getDataSourceSrv().get('-- Mixed --'); - from(ds.query(requestMixed)).subscribe(result => { - results.push(result); - if (result.state === LoadingState.Done) { - done(); - } + from(ds.query(requestMixed)).subscribe(result => { + results.push(result); + if (result.state === LoadingState.Done) { + done(); + } + }); + }); + + it('direct query should return results', async () => { + expect(results.length).toBe(3); + expect(results[0].data).toEqual(['AAAA']); + expect(results[0].state).toEqual(LoadingState.Loading); + expect(results[1].data).toEqual(['BBBB']); + expect(results[2].data).toEqual(['CCCC']); + expect(results[2].state).toEqual(LoadingState.Done); + expect(results.length).toBe(3); }); }); - it('direct query should return results', async () => { - expect(results.length).toBe(3); - expect(results[0].data).toEqual(['AAAA']); - expect(results[0].state).toEqual(LoadingState.Loading); - expect(results[1].data).toEqual(['BBBB']); - expect(results[2].data).toEqual(['CCCC']); - expect(results[2].state).toEqual(LoadingState.Done); - expect(results.length).toBe(3); + describe('with errors', () => { + const requestMixed = getQueryOptions({ + targets: [ + { refId: 'QA', datasource: 'A' }, // 1 + { refId: 'QD', datasource: 'D' }, // 2 + { refId: 'QB', datasource: 'B' }, // 3 + { refId: 'QE', datasource: 'E' }, // 4 + { refId: 'QC', datasource: 'C' }, // 5 + ], + }); + const results: any[] = []; + + beforeEach(async done => { + const ds = await getDataSourceSrv().get('-- Mixed --'); + + from(ds.query(requestMixed)).subscribe(result => { + results.push(result); + if (results.length === 5) { + done(); + } + }); + }); + + it('direct query should return results', async () => { + expect(results[0].data).toEqual(['AAAA']); + expect(results[0].state).toEqual(LoadingState.Loading); + expect(results[1].data).toEqual([]); + expect(results[1].state).toEqual(LoadingState.Error); + expect(results[1].error).toEqual({ message: 'DSD: syntax error near FROM' }); + expect(results[2].data).toEqual(['BBBB']); + expect(results[2].state).toEqual(LoadingState.Loading); + expect(results[3].data).toEqual([]); + expect(results[3].state).toEqual(LoadingState.Error); + expect(results[3].error).toEqual({ message: 'DSE: syntax error near WHERE' }); + expect(results[4].data).toEqual(['CCCC']); + expect(results[4].state).toEqual(LoadingState.Loading); + expect(results[5].data).toEqual([]); + expect(results[5].state).toEqual(LoadingState.Error); + expect(results[5].error).toEqual({ message: 'DSD: syntax error near FROM' }); + }); }); }); diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.ts b/public/app/plugins/datasource/mixed/MixedDataSource.ts index 9ddc0dc24b1..c97e09997d6 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.ts @@ -1,17 +1,17 @@ import cloneDeep from 'lodash/cloneDeep'; import groupBy from 'lodash/groupBy'; -import { from, of, Observable, forkJoin } from 'rxjs'; -import { map, mergeMap, mergeAll } from 'rxjs/operators'; +import { forkJoin, from, Observable, of } from 'rxjs'; +import { catchError, map, mergeAll, mergeMap } from 'rxjs/operators'; import { - LoadingState, - DataSourceApi, DataQuery, DataQueryRequest, DataQueryResponse, + DataSourceApi, DataSourceInstanceSettings, + LoadingState, } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; +import { getDataSourceSrv, toDataQueryError } from '@grafana/runtime'; export const MIXED_DATASOURCE_NAME = '-- Mixed --'; @@ -68,13 +68,25 @@ export class MixedDatasource extends DataSourceApi { state: LoadingState.Loading, key: `mixed-${i}-${response.key || ''}`, } as DataQueryResponse; + }), + catchError(err => { + err = toDataQueryError(err); + + err.message = `${api.name}: ${err.message}`; + + return of({ + data: [], + state: LoadingState.Error, + error: err, + key: `mixed-${i}-${dsRequest.requestId || ''}`, + }); }) ); }) ) ); - return forkJoin(runningQueries).pipe(map(this.markAsDone), mergeAll()); + return forkJoin(runningQueries).pipe(map(this.finalizeResponses), mergeAll()); } testDatasource() { @@ -85,14 +97,20 @@ export class MixedDatasource extends DataSourceApi { return query && Array.isArray(query.targets) && query.targets.length > 0; } - private markAsDone(responses: DataQueryResponse[]): DataQueryResponse[] { + private finalizeResponses(responses: DataQueryResponse[]): DataQueryResponse[] { const { length } = responses; if (length === 0) { return responses; } - responses[length - 1].state = LoadingState.Done; + const error = responses.find(response => response.state === LoadingState.Error); + if (error) { + responses.push(error); // adds the first found error entry so error shows up in the panel + } else { + responses[length - 1].state = LoadingState.Done; + } + return responses; } } diff --git a/public/test/mocks/datasource_srv.ts b/public/test/mocks/datasource_srv.ts index f97e27f9d3d..3f3b8527563 100644 --- a/public/test/mocks/datasource_srv.ts +++ b/public/test/mocks/datasource_srv.ts @@ -1,7 +1,7 @@ import { - DataSourceApi, DataQueryRequest, DataQueryResponse, + DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta, } from '@grafana/data'; @@ -27,7 +27,7 @@ export class MockDataSourceApi extends DataSourceApi { result: DataQueryResponse = { data: [] }; queryResolver: Promise; - constructor(name?: string, result?: DataQueryResponse, meta?: any) { + constructor(name?: string, result?: DataQueryResponse, meta?: any, private error: string | null = null) { super({ name: name ? name : 'MockDataSourceApi' } as DataSourceInstanceSettings); if (result) { this.result = result; @@ -40,6 +40,11 @@ export class MockDataSourceApi extends DataSourceApi { if (this.queryResolver) { return this.queryResolver; } + + if (this.error) { + return Promise.reject(this.error); + } + return new Promise(resolver => { setTimeout(() => { resolver(this.result);