Explore: Fix broken interpolation for mixed datasources coming from dashboards (#76904)

* Fix broken interpolation for mixed datasources in explore

* Add interpolation tests

* Use Promise.all and be better about dealing with invalid empty ds in query scenario

* condense logic

* Remove type declarations

* Fix tests
This commit is contained in:
Kristina 2023-10-26 08:16:22 -05:00 committed by GitHub
parent 5ff550ae19
commit 2c92e370dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 31 deletions

View File

@ -1,5 +1,6 @@
import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
import { DataSourceApi, dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { DataQuery } from '@grafana/schema';
import { RefreshPicker } from '@grafana/ui';
import store from 'app/core/store';
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
@ -24,12 +25,36 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
};
const defaultDs = new MockDataSourceApi('default datasource', { data: ['default data'] });
const interpolateMockLoki = jest
.fn()
.mockReturnValue([{ refId: 'a', expr: 'replaced testDs loki' }]) as unknown as DataQuery[];
const interpolateMockProm = jest
.fn()
.mockReturnValue([{ refId: 'a', expr: 'replaced testDs2 prom' }]) as unknown as DataQuery[];
const datasourceSrv = new DatasourceSrvMock(defaultDs, {
'generate empty query': new MockDataSourceApi('generateEmptyQuery'),
ds1: {
name: 'testDs',
type: 'loki',
meta: { mixed: false },
interpolateVariablesInQueries: interpolateMockLoki,
getRef: () => {
return 'ds1';
},
} as unknown as DataSourceApi,
ds2: {
name: 'testDs2',
type: 'prom',
meta: { mixed: false },
interpolateVariablesInQueries: interpolateMockProm,
getRef: () => {
return 'ds2';
},
} as unknown as DataSourceApi,
dsMixed: {
name: 'testDSMixed',
type: 'mixed',
meta: { mixed: true },
} as MockDataSourceApi,
});
@ -70,6 +95,10 @@ describe('state functions', () => {
});
describe('getExploreUrl', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const args = {
queries: [
{ refId: 'A', expr: 'query1', legendFormat: 'legendFormat1' },
@ -86,6 +115,40 @@ describe('getExploreUrl', () => {
it('should omit expression target in explore url', async () => {
expect(await getExploreUrl(args)).not.toMatch(/__expr__/g);
});
it('should interpolate queries with variables in a non-mixed datasource scenario', async () => {
// this is not actually valid (see root and query DS being different) but it will test the root DS mock was called
const nonMixedArgs = {
queries: [{ refId: 'A', expr: 'query1', datasource: { type: 'prom', uid: 'ds2' } }],
dsRef: {
uid: 'ds1',
meta: { mixed: false },
},
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } },
scopedVars: {},
};
expect(await getExploreUrl(nonMixedArgs)).toMatch(/replaced%20testDs2%20prom/g);
expect(interpolateMockLoki).not.toBeCalled();
expect(interpolateMockProm).toBeCalled();
});
it('should interpolate queries with variables in a mixed datasource scenario', async () => {
const nonMixedArgs = {
queries: [
{ refId: 'A', expr: 'query1', datasource: { type: 'loki', uid: 'ds1' } },
{ refId: 'B', expr: 'query2', datasource: { type: 'prom', uid: 'ds2' } },
],
dsRef: {
uid: 'dsMixed',
meta: { mixed: true },
},
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } },
scopedVars: {},
};
const url = await getExploreUrl(nonMixedArgs);
expect(url).toMatch(/replaced%20testDs%20loki/g);
expect(url).toMatch(/replaced%20testDs2%20prom/g);
expect(interpolateMockLoki).toBeCalled();
expect(interpolateMockProm).toBeCalled();
});
});
describe('updateHistory()', () => {

View File

@ -9,7 +9,6 @@ import {
DataSourceApi,
DataSourceRef,
DefaultTimeZone,
ExploreUrlState,
HistoryItem,
IntervalValues,
LogsDedupStrategy,
@ -61,36 +60,36 @@ export function generateExploreId() {
*/
export async function getExploreUrl(args: GetExploreUrlArguments): Promise<string | undefined> {
const { queries, dsRef, timeRange, scopedVars } = args;
let exploreDatasource = await getDataSourceSrv().get(dsRef);
const interpolatedQueries = (
await Promise.allSettled(
queries
// Explore does not support expressions so filter those out
.filter((q) => q.datasource?.uid !== ExpressionDatasourceUID)
.map(async (q) => {
// if the query defines a datasource, use that one, otherwise use the one from the panel, which should always be defined.
// this will rejects if the datasource is not found, or return the default one if dsRef is not provided.
const queryDs = await getDataSourceSrv().get(q.datasource || dsRef);
/*
* Explore does not support expressions so filter those out
*/
let exploreTargets: DataQuery[] = queries.filter((t) => t.datasource?.uid !== ExpressionDatasourceUID);
return {
// interpolate the query using its datasource `interpolateVariablesInQueries` method if defined, othewise return the query as-is.
...(queryDs.interpolateVariablesInQueries?.([q], scopedVars ?? {})[0] || q),
// But always set the datasource as it's required in Explore.
// NOTE: if for some reason the query has the "mixed" datasource, we omit the property;
// Upon initialization, Explore use its own logic to determine the datasource.
...(!queryDs.meta.mixed && { datasource: queryDs.getRef() }),
};
})
)
)
.filter(
<T>(promise: PromiseSettledResult<T>): promise is PromiseFulfilledResult<T> => promise.status === 'fulfilled'
)
.map((q) => q.value);
let url: string | undefined;
if (exploreDatasource) {
let state: Partial<ExploreUrlState> = { range: toURLRange(timeRange.raw) };
if (exploreDatasource.interpolateVariablesInQueries) {
state = {
...state,
datasource: exploreDatasource.uid,
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars ?? {}),
};
} else {
state = {
...state,
datasource: exploreDatasource.uid,
queries: exploreTargets,
};
}
const exploreState = JSON.stringify({ [generateExploreId()]: state });
url = urlUtil.renderUrl('/explore', { panes: exploreState, schemaVersion: 1 });
}
return url;
const exploreState = JSON.stringify({
[generateExploreId()]: { range: toURLRange(timeRange.raw), queries: interpolatedQueries, datasource: dsRef?.uid },
});
return urlUtil.renderUrl('/explore', { panes: exploreState, schemaVersion: 1 });
}
export function buildQueryTransaction(