Explore: Add Mixed Datasource (#51605)

* Toggle on the mixed mode option

* Ensure switching to mixed gives existing query prev datasource

* WIP - Populate datasource when switching between mixed and not

* WIP - handle change from mixed

* Remove preimport filter, refine filter to work for queries

* WIP debugging datasource transition

* Ensure creating a new query gets target data source if switching with no matches between

* Add mixed datasource to rich history display

* Cleanup console logs, add relevant comments

* Add feature toggle for mixed datasource

* Fix Wrapper tests

* Fix tests!

* Fix test types and add feature tracking

* Remove unnecessary default, remove explore/mixed workarounds for D2E

* Move display text logic to mixed datasource file

* Add in the default query parameters to a generated empty query

* Condense some code

* Apply suggestions from code review

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
Kristina 2022-07-27 09:40:59 -05:00 committed by GitHub
parent a98920f910
commit e2258120e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 194 additions and 87 deletions

View File

@ -1208,7 +1208,7 @@ lokiQueryBuilder = true
# Experimental Explore to Dashboard workflow
explore2Dashboard = true
# Experimental Command Palette
# Command Palette
commandPalette = true
# Use dynamic labels in CloudWatch datasource

View File

@ -73,3 +73,7 @@ The Share shortened link capability allows you to create smaller and simpler URL
> **Note:** Available in Grafana 8.5.0 and later versions.
Enabled by default, allows users to create panels in dashboards from within Explore.
### exploreMixedDatasource
Disabled by default, allows users in Explore to have different datasources for different queries. If compatible, results will be combined.

View File

@ -44,6 +44,7 @@ export interface FeatureToggles {
export?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
explore2Dashboard?: boolean;
exploreMixedDatasource?: boolean;
tracing?: boolean;
commandPalette?: boolean;
cloudWatchDynamicLabels?: boolean;

View File

@ -159,6 +159,12 @@ var (
State: FeatureStateBeta,
FrontendOnly: true,
},
{
Name: "exploreMixedDatasource",
Description: "Enable mixed datasource in Explore",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "tracing",
Description: "Adds trace ID to error notifications",

View File

@ -119,6 +119,10 @@ const (
// Experimental Explore to Dashboard workflow
FlagExplore2Dashboard = "explore2Dashboard"
// FlagExploreMixedDatasource
// Enable mixed datasource in Explore
FlagExploreMixedDatasource = "exploreMixedDatasource"
// FlagTracing
// Adds trace ID to error notifications
FlagTracing = "tracing"

View File

@ -7,6 +7,7 @@ import {
DataQuery,
DataQueryRequest,
DataSourceApi,
DataSourceRef,
dateMath,
DateTime,
DefaultTimeZone,
@ -24,7 +25,7 @@ import {
toUtc,
urlUtil,
} from '@grafana/data';
import { DataSourceSrv } from '@grafana/runtime';
import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime';
import { RefreshPicker } from '@grafana/ui';
import store from 'app/core/store';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -70,19 +71,6 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
let exploreTargets: DataQuery[] = panel.targets.map((t) => omit(t, 'legendFormat'));
let url: string | undefined;
// Mixed datasources need to choose only one datasource
if (exploreDatasource.meta?.id === 'mixed' && exploreTargets) {
// Find first explore datasource among targets
for (const t of exploreTargets) {
const datasource = await datasourceSrv.get(t.datasource || undefined);
if (datasource) {
exploreDatasource = datasource;
exploreTargets = panel.targets.filter((t) => t.datasource === datasource.name);
break;
}
}
}
if (exploreDatasource) {
const range = timeSrv.timeRangeForUrl();
let state: Partial<ExploreUrlState> = { range };
@ -99,7 +87,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
...state,
datasource: exploreDatasource.name,
context: 'explore',
queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })),
queries: exploreTargets,
};
}
@ -254,8 +242,34 @@ export function generateKey(index = 0): string {
return `Q-${uuidv4()}-${index}`;
}
export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
return { refId: getNextRefIdChar(queries), key: generateKey(index) };
export async function generateEmptyQuery(
queries: DataQuery[],
index = 0,
dataSourceOverride?: DataSourceRef
): Promise<DataQuery> {
let datasourceInstance: DataSourceApi | undefined;
let datasourceRef: DataSourceRef | null | undefined;
let defaultQuery: Partial<DataQuery> | undefined;
// datasource override is if we have switched datasources with no carry-over - we want to create a new query with a datasource we define
if (dataSourceOverride) {
datasourceRef = dataSourceOverride;
} else if (queries.length > 0 && queries[queries.length - 1].datasource) {
// otherwise use last queries' datasource
datasourceRef = queries[queries.length - 1].datasource;
} else {
// if neither exists, use the default datasource
datasourceInstance = await getDataSourceSrv().get();
defaultQuery = datasourceInstance.getDefaultQuery?.(CoreApp.Explore);
datasourceRef = datasourceInstance.getRef();
}
if (!datasourceInstance) {
datasourceInstance = await getDataSourceSrv().get(datasourceRef);
defaultQuery = datasourceInstance.getDefaultQuery?.(CoreApp.Explore);
}
return { refId: getNextRefIdChar(queries), key: generateKey(index), datasource: datasourceRef, ...defaultQuery };
}
export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => {
@ -267,8 +281,13 @@ export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: D
/**
* Ensure at least one target exists and that targets have the necessary keys
*
* This will return an empty array if there are no datasources, as Explore is not usable in that state
*/
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
export async function ensureQueries(
queries?: DataQuery[],
newQueryDataSourceOverride?: DataSourceRef
): Promise<DataQuery[]> {
if (queries && typeof queries === 'object' && queries.length > 0) {
const allQueries = [];
for (let index = 0; index < queries.length; index++) {
@ -287,7 +306,18 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
}
return allQueries;
}
return [{ ...generateEmptyQuery(queries ?? []) }];
try {
// if a datasourse override get its ref, otherwise get the default datasource
const emptyQueryRef = newQueryDataSourceOverride ?? (await getDataSourceSrv().get()).getRef();
const emptyQuery = await generateEmptyQuery(queries ?? [], undefined, emptyQueryRef);
return [emptyQuery];
} catch {
// if there are no datasources, return an empty array because we will not allow use of explore
// this will occur on init of explore with no datasources defined
return [];
}
}
/**
@ -344,9 +374,9 @@ export function clearHistory(datasourceId: string) {
store.delete(historyKey);
}
export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSourceApi | null): string[] => {
export const getQueryKeys = (queries: DataQuery[]): string[] => {
const queryKeys = queries.reduce<string[]>((newQueryKeys, query, index) => {
const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
const primaryKey = query.datasource?.uid || query.key;
return newQueryKeys.concat(`${primaryKey}-${index}`);
}, []);

View File

@ -264,7 +264,7 @@ export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortO
*/
export function createDatasourcesList() {
return getDataSourceSrv()
.getList()
.getList({ mixed: true })
.map((dsSettings) => {
return {
name: dsSettings.name,

View File

@ -158,8 +158,8 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
};
onClickAddQueryRowButton = () => {
const { exploreId, queryKeys, datasourceInstance } = this.props;
this.props.addQueryRow(exploreId, queryKeys.length, datasourceInstance);
const { exploreId, queryKeys } = this.props;
this.props.addQueryRow(exploreId, queryKeys.length);
};
onMakeAbsoluteTime = () => {

View File

@ -3,7 +3,7 @@ import memoizeOne from 'memoize-one';
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataQuery, ExploreUrlState, EventBusExtended, EventBusSrv, GrafanaTheme2 } from '@grafana/data';
import { ExploreUrlState, EventBusExtended, EventBusSrv, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Themeable2, withTheme2 } from '@grafana/ui';
import store from 'app/core/store';
@ -64,16 +64,17 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
};
}
componentDidMount() {
async componentDidMount() {
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, panelsState } = this.props;
const width = this.el?.offsetWidth ?? 0;
// initialize the whole explore first time we mount and if browser history contains a change in datasource
if (!initialized) {
const queries = await ensureQueries(initialQueries); // this will return an empty array if there are no datasources
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
queries,
initialRange,
width,
this.exploreEvents,
@ -116,7 +117,6 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
}
}
const ensureQueriesMemoized = memoizeOne(ensureQueries);
const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, props: OwnProps) {
@ -126,7 +126,6 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
const { datasource, queries, range: urlRange, panelsState } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
const initialRange = urlRange
? getTimeRangeFromUrlMemoized(urlRange, timeZone, fiscalYearStartMonth)
: getTimeRange(timeZone, DEFAULT_RANGE, fiscalYearStartMonth);
@ -134,7 +133,7 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
return {
initialized: state.explore[props.exploreId]?.initialized,
initialDatasource,
initialQueries,
initialQueries: queries,
initialRange,
panelsState,
};

View File

@ -145,6 +145,7 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
mixed={config.featureToggles.exploreMixedDatasource === true}
onChange={this.onChangeDatasource}
current={this.props.datasourceRef}
hideTextValue={showSmallDataSourcePicker}

View File

@ -39,8 +39,7 @@ describe('Wrapper', () => {
it('shows warning if there are no data sources', async () => {
setupExplore({ datasources: [] });
// Will throw if isn't found
screen.getByText(/Explore requires at least one data source/i);
await waitFor(() => screen.getByText(/Explore requires at least one data source/i));
});
it('inits url and renders editor but does not call query on empty url', async () => {
@ -52,7 +51,7 @@ describe('Wrapper', () => {
orgId: '1',
left: serializeStateToUrlParam({
datasource: 'loki',
queries: [{ refId: 'A' }],
queries: [{ refId: 'A', datasource: { type: 'logs', uid: 'loki' } }],
range: { from: 'now-1h', to: 'now' },
}),
});
@ -144,7 +143,7 @@ describe('Wrapper', () => {
orgId: '1',
left: serializeStateToUrlParam({
datasource: 'elastic',
queries: [{ refId: 'A' }],
queries: [{ refId: 'A', datasource: { type: 'logs', uid: 'elastic' } }],
range: { from: 'now-1h', to: 'now' },
}),
});

View File

@ -32,7 +32,7 @@ type SetupOptions = {
};
export function setupExplore(options?: SetupOptions): {
datasources: { [name: string]: DataSourceApi };
datasources: { [uid: string]: DataSourceApi };
store: ReturnType<typeof configureStore>;
unmount: () => void;
container: HTMLElement;
@ -58,15 +58,19 @@ export function setupExplore(options?: SetupOptions): {
getInstanceSettings(ref: DataSourceRef) {
return dsSettings.map((d) => d.settings).find((x) => x.name === ref || x.uid === ref || x.uid === ref.uid);
},
get(datasource?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
const datasourceStr = typeof datasource === 'string';
return Promise.resolve(
(datasource
? dsSettings.find((d) =>
datasourceStr ? d.api.name === datasource || d.api.uid === datasource : d.api.uid === datasource?.uid
)
: dsSettings[0])!.api
);
get(datasource?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise<DataSourceApi | undefined> {
if (dsSettings.length === 0) {
return Promise.resolve(undefined);
} else {
const datasourceStr = typeof datasource === 'string';
return Promise.resolve(
(datasource
? dsSettings.find((d) =>
datasourceStr ? d.api.name === datasource || d.api.uid === datasource : d.api.uid === datasource?.uid
)
: dsSettings[0])!.api
);
}
},
} as any);
@ -149,7 +153,7 @@ function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: nu
name: name,
uid: name,
query: jest.fn(),
getRef: jest.fn().mockReturnValue(name),
getRef: jest.fn().mockReturnValue({ type: 'logs', uid: name }),
meta,
} as any,
};

View File

@ -2,6 +2,7 @@
import { AnyAction, createAction } from '@reduxjs/toolkit';
import { DataSourceApi, HistoryItem } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { RefreshPicker } from '@grafana/ui';
import { stopQueryState } from 'app/core/utils/explore';
import { ExploreItemState, ThunkResult } from 'app/types';
@ -44,6 +45,11 @@ export function changeDatasource(
const { history, instance } = await loadAndInitDatasource(orgId, { uid: datasourceUid });
const currentDataSourceInstance = getState().explore[exploreId]!.datasourceInstance;
reportInteraction('explore_change_ds', {
from: (currentDataSourceInstance?.meta?.mixed ? 'mixed' : currentDataSourceInstance?.type) || 'unknown',
to: instance.meta.mixed ? 'mixed' : instance.type,
exploreId,
});
dispatch(
updateDatasourceInstanceAction({
exploreId,

View File

@ -72,6 +72,9 @@ function setup(state?: any) {
testDatasource: jest.fn(),
init: jest.fn(),
name: 'default',
getRef() {
return { type: 'default', uid: 'default' };
},
}
);
},

View File

@ -222,7 +222,7 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
// commit changes based on the diff of new url vs old url
if (update.datasource) {
const initialQueries = ensureQueries(queries);
const initialQueries = await ensureQueries(queries);
await dispatch(
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState)
);
@ -304,7 +304,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
range,
queries,
initialized: true,
queryKeys: getQueryKeys(queries, datasourceInstance),
queryKeys: getQueryKeys(queries),
datasourceInstance,
history,
datasourceMissing: !datasourceInstance,

View File

@ -154,12 +154,31 @@ describe('running queries', () => {
describe('importing queries', () => {
describe('when importing queries between the same type of data source', () => {
it('remove datasource property from all of the queries', async () => {
const datasources: DataSourceApi[] = [
{
name: 'testDs',
type: 'postgres',
uid: 'ds1',
getRef: () => {
return { type: 'postgres', uid: 'ds1' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{
name: 'testDs2',
type: 'postgres',
uid: 'ds2',
getRef: () => {
return { type: 'postgres', uid: 'ds2' };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
];
const { dispatch, getState }: { dispatch: ThunkDispatch; getState: () => StoreState } = configureStore({
...(defaultInitialState as any),
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
datasourceInstance: { name: 'testDs', type: 'postgres' },
datasourceInstance: datasources[0],
},
},
});
@ -168,18 +187,18 @@ describe('importing queries', () => {
importQueries(
ExploreId.left,
[
{ datasource: { type: 'postgresql' }, refId: 'refId_A' },
{ datasource: { type: 'postgresql' }, refId: 'refId_B' },
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_A' },
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_B' },
],
{ name: 'Postgres1', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>,
{ name: 'Postgres2', type: 'postgres' } as DataSourceApi<DataQuery, DataSourceJsonData, {}>
datasources[0],
datasources[1]
)
);
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('refId', 'refId_A');
expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('refId', 'refId_B');
expect(getState().explore[ExploreId.left].queries[0]).not.toHaveProperty('datasource');
expect(getState().explore[ExploreId.left].queries[1]).not.toHaveProperty('datasource');
expect(getState().explore[ExploreId.left].queries[0]).toHaveProperty('datasource.uid', 'ds2');
expect(getState().explore[ExploreId.left].queries[1]).toHaveProperty('datasource.uid', 'ds2');
});
});
});

View File

@ -1,11 +1,11 @@
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
import deepEqual from 'fast-deep-equal';
import { flatten, groupBy } from 'lodash';
import { identity, Observable, of, SubscriptionLike, Unsubscribable } from 'rxjs';
import { mergeMap, throttleTime } from 'rxjs/operators';
import {
AbsoluteTimeRange,
CoreApp,
DataQuery,
DataQueryErrorType,
DataQueryResponse,
@ -20,7 +20,7 @@ import {
QueryFixAction,
toLegacyResponseData,
} from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import {
buildQueryTransaction,
ensureQueries,
@ -33,6 +33,7 @@ import {
} from 'app/core/utils/explore';
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ExploreItemState, ExplorePanelData, ThunkDispatch, ThunkResult } from 'app/types';
import { ExploreId, ExploreState, QueryOptions } from 'app/types/explore';
@ -214,17 +215,10 @@ export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCa
/**
* Adds a query row after the row with the given index.
*/
export function addQueryRow(
exploreId: ExploreId,
index: number,
datasource: DataSourceApi | undefined | null
): ThunkResult<void> {
return (dispatch, getState) => {
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return async (dispatch, getState) => {
const queries = getState().explore[exploreId]!.queries;
const query = {
...datasource?.getDefaultQuery?.(CoreApp.Explore),
...generateEmptyQuery(queries, index),
};
const query = await generateEmptyQuery(queries, index);
dispatch(addQueryRowAction({ exploreId, index, query }));
};
@ -251,6 +245,32 @@ export function cancelQueries(exploreId: ExploreId): ThunkResult<void> {
};
}
const addDatasourceToQueries = (datasource: DataSourceApi, queries: DataQuery[]) => {
const dataSourceRef = datasource.getRef();
return queries.map((query: DataQuery) => {
return { ...query, datasource: dataSourceRef };
});
};
const getImportableQueries = async (
targetDataSource: DataSourceApi,
sourceDataSource: DataSourceApi,
queries: DataQuery[]
): Promise<DataQuery[]> => {
let queriesOut: DataQuery[] = [];
if (sourceDataSource.meta?.id === targetDataSource.meta?.id) {
queriesOut = queries;
} else if (hasQueryExportSupport(sourceDataSource) && hasQueryImportSupport(targetDataSource)) {
const abstractQueries = await sourceDataSource.exportToAbstractQueries(queries);
queriesOut = await targetDataSource.importFromAbstractQueries(abstractQueries);
} else if (targetDataSource.importQueries) {
// Datasource-specific importers
queriesOut = await targetDataSource.importQueries(queries, sourceDataSource);
}
// add new datasource to queries before returning
return addDatasourceToQueries(targetDataSource, queriesOut);
};
/**
* Import queries from previous datasource if possible eg Loki and Prometheus have similar query language so the
* labels part can be reused to get similar data.
@ -273,23 +293,27 @@ export const importQueries = (
}
let importedQueries = queries;
// Check if queries can be imported from previously selected datasource
if (sourceDataSource.meta?.id === targetDataSource.meta?.id) {
// Keep same queries if same type of datasource, but delete datasource query property to prevent mismatch of new and old data source instance
importedQueries = queries.map(({ datasource, ...query }) => query);
} else if (hasQueryExportSupport(sourceDataSource) && hasQueryImportSupport(targetDataSource)) {
const abstractQueries = await sourceDataSource.exportToAbstractQueries(queries);
importedQueries = await targetDataSource.importFromAbstractQueries(abstractQueries);
} else if (targetDataSource.importQueries) {
// Datasource-specific importers
importedQueries = await targetDataSource.importQueries(queries, sourceDataSource);
// If going to mixed, keep queries with source datasource
if (targetDataSource.name === MIXED_DATASOURCE_NAME) {
importedQueries = queries.map((query) => {
return { ...query, datasource: sourceDataSource.getRef() };
});
}
// If going from mixed, see what queries you keep by their individual datasources
else if (sourceDataSource.name === MIXED_DATASOURCE_NAME) {
const groupedQueries = groupBy(queries, (query) => query.datasource?.uid);
const groupedImportableQueries = await Promise.all(
Object.keys(groupedQueries).map(async (key: string) => {
const queryDatasource = await getDataSourceSrv().get({ uid: key });
return await getImportableQueries(targetDataSource, queryDatasource, groupedQueries[key]);
})
);
importedQueries = flatten(groupedImportableQueries.filter((arr) => arr.length > 0));
} else {
// Default is blank queries
importedQueries = ensureQueries();
importedQueries = await getImportableQueries(targetDataSource, sourceDataSource, queries);
}
const nextQueries = ensureQueries(importedQueries);
const nextQueries = await ensureQueries(importedQueries, targetDataSource.getRef());
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
};
};
@ -639,7 +663,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries: nextQueries,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
queryKeys: getQueryKeys(nextQueries),
};
}
@ -685,7 +709,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries: nextQueries,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
queryKeys: getQueryKeys(nextQueries),
};
}
@ -694,7 +718,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries: queries.slice(),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
queryKeys: getQueryKeys(queries),
};
}
@ -703,7 +727,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries,
queryKeys: getQueryKeys(queries, state.datasourceInstance),
queryKeys: getQueryKeys(queries),
};
}
@ -760,7 +784,7 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
return {
...state,
queries,
queryKeys: getQueryKeys(queries, state.datasourceInstance),
queryKeys: getQueryKeys(queries),
};
}

View File

@ -1,4 +1,4 @@
import { cloneDeep, groupBy } from 'lodash';
import { cloneDeep, groupBy, omit } from 'lodash';
import { forkJoin, from, Observable, of, OperatorFunction } from 'rxjs';
import { catchError, map, mergeAll, mergeMap, reduce, toArray } from 'rxjs/operators';
@ -98,6 +98,13 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
return Promise.resolve({});
}
getQueryDisplayText(query: DataQuery) {
const strippedQuery = omit(query, ['key', 'refId', 'datasource']);
const strippedQueryJSON = JSON.stringify(strippedQuery);
const prefix = query.datasource?.type ? `${query.datasource?.type}: ` : '';
return `${prefix}${strippedQueryJSON}`;
}
private isQueryable(query: BatchedQueries): boolean {
return query && Array.isArray(query.targets) && query.targets.length > 0;
}