diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 8a95b3ef9ee..aea4ce3bda9 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -13,7 +13,7 @@ import { storeLogsVolumeDataProviderAction, } from './query'; import { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; -import { interval, Observable, of } from 'rxjs'; +import { EMPTY, interval, Observable, of } from 'rxjs'; import { ArrayVector, DataFrame, @@ -91,10 +91,8 @@ function setupQueryResponse(state: StoreState) { describe('runQueries', () => { it('should pass dataFrames to state even if there is error in response', async () => { - setTimeSrv({ - init() {}, - } as any); - const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({ + setTimeSrv({ init() {} } as any); + const { dispatch, getState } = configureStore({ ...(defaultInitialState as any), }); setupQueryResponse(getState()); @@ -102,6 +100,17 @@ describe('runQueries', () => { expect(getState().explore[ExploreId.left].showMetrics).toBeTruthy(); expect(getState().explore[ExploreId.left].graphResult).toBeDefined(); }); + + it('should set state to done if query completes without emitting', async () => { + setTimeSrv({ init() {} } as any); + const { dispatch, getState } = configureStore({ + ...(defaultInitialState as any), + }); + (getState().explore[ExploreId.left].datasourceInstance?.query as Mock).mockReturnValueOnce(EMPTY); + await dispatch(runQueries(ExploreId.left)); + await new Promise((resolve) => setTimeout(() => resolve(''), 500)); + expect(getState().explore[ExploreId.left].queryResponse.state).toBe(LoadingState.Done); + }); }); describe('running queries', () => { diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 6e0dc801329..e4eee245df3 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -449,8 +449,8 @@ export const runQueries = ( ) ) ) - .subscribe( - (data) => { + .subscribe({ + next(data) { dispatch(queryStreamUpdatedAction({ exploreId, response: data })); // Keep scanning for results if this was the last scanning transaction @@ -465,12 +465,15 @@ export const runQueries = ( } } }, - (error) => { + error(error) { dispatch(notifyApp(createErrorNotification('Query processing error', error))); dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Error })); console.error(error); - } - ); + }, + complete() { + dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Done })); + }, + }); if (live) { dispatch( diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 7e6ff2ef9a8..423e195d9b1 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -16,6 +16,15 @@ import { DEFAULT_LIMIT, TempoJsonData, TempoDatasource, TempoQuery } from './dat import mockJson from './mockJsonResponse.json'; describe('Tempo data source', () => { + it('returns empty response when traceId is empty', async () => { + const ds = new TempoDatasource(defaultSettings); + const response = await lastValueFrom( + ds.query({ targets: [{ refId: 'refid1', queryType: 'traceId', query: '' } as Partial] } as any), + { defaultValue: 'empty' } + ); + expect(response).toBe('empty'); + }); + it('parses json fields from backend', async () => { setupBackendSrv( new MutableDataFrame({ @@ -34,7 +43,7 @@ describe('Tempo data source', () => { }) ); const ds = new TempoDatasource(defaultSettings); - const response = await lastValueFrom(ds.query({ targets: [{ refId: 'refid1' }] } as any)); + const response = await lastValueFrom(ds.query({ targets: [{ refId: 'refid1', query: '12345' }] } as any)); expect( (response.data[0] as DataFrame).fields.map((f) => ({ diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index d70e58680aa..3908e93db11 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -1,4 +1,4 @@ -import { from, merge, Observable, of, throwError } from 'rxjs'; +import { EMPTY, from, merge, Observable, of, throwError } from 'rxjs'; import { catchError, map, mergeMap, toArray } from 'rxjs/operators'; import { DataQuery, @@ -41,7 +41,7 @@ export interface TempoJsonData extends DataSourceJsonData { nodeGraph?: NodeGraphOptions; } -export type TempoQuery = { +export interface TempoQuery extends DataQuery { query: string; // Query to find list of traces, e.g., via Loki linkedQuery?: LokiQuery; @@ -53,7 +53,7 @@ export type TempoQuery = { maxDuration?: string; limit?: number; serviceMapQuery?: string; -} & DataQuery; +} export const DEFAULT_LIMIT = 20; @@ -152,22 +152,38 @@ export class TempoDatasource extends DataSourceWithBackend 0) { - const traceRequest: DataQueryRequest = { ...options, targets: targets.traceId }; - subQueries.push( - super.query(traceRequest).pipe( - map((response) => { - if (response.error) { - return response; - } - return transformTrace(response, this.nodeGraph?.enabled); - }) - ) - ); + subQueries.push(this.handleTraceIdQuery(options, targets.traceId)); } return merge(...subQueries); } + /** + * Handles the simplest of the queries where we have just a trace id and return trace data for it. + * @param options + * @param targets + * @private + */ + private handleTraceIdQuery( + options: DataQueryRequest, + targets: TempoQuery[] + ): Observable { + const validTargets = targets.filter((t) => t.query); + if (!validTargets.length) { + return EMPTY; + } + + const traceRequest: DataQueryRequest = { ...options, targets: validTargets }; + return super.query(traceRequest).pipe( + map((response) => { + if (response.error) { + return response; + } + return transformTrace(response, this.nodeGraph?.enabled); + }) + ); + } + async metadataRequest(url: string, params = {}) { return await this._request(url, params, { method: 'GET', hideFromInspector: true }).toPromise(); }