diff --git a/conf/defaults.ini b/conf/defaults.ini index 51c835d1822..218efc49054 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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 diff --git a/docs/sources/explore/_index.md b/docs/sources/explore/_index.md index 190fb78c2e4..013e01db4c2 100644 --- a/docs/sources/explore/_index.md +++ b/docs/sources/explore/_index.md @@ -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. diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index c59f8e728f0..c69788424f4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -44,6 +44,7 @@ export interface FeatureToggles { export?: boolean; azureMonitorResourcePickerForMetrics?: boolean; explore2Dashboard?: boolean; + exploreMixedDatasource?: boolean; tracing?: boolean; commandPalette?: boolean; cloudWatchDynamicLabels?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e38dc69157e..ed284fb8f8f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 35a6b40560c..239a454c9ab 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index a28a58909bb..27d60caa9e0 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -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 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 = { range }; @@ -99,7 +87,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise ({ ...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 { + let datasourceInstance: DataSourceApi | undefined; + let datasourceRef: DataSourceRef | null | undefined; + let defaultQuery: Partial | 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 { 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((newQueryKeys, query, index) => { - const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key; + const primaryKey = query.datasource?.uid || query.key; return newQueryKeys.concat(`${primaryKey}-${index}`); }, []); diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index c2c34ef50e3..fc66871eedd 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -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, diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 55233403242..32124d86453 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -158,8 +158,8 @@ export class Explore extends React.PureComponent { }; 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 = () => { diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 0e7dc124a1f..44604f849d5 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -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 { }; } - 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 { } } -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, }; diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 34ded3050a7..8bc68cbc009 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -145,6 +145,7 @@ class UnConnectedExploreToolbar extends PureComponent { !datasourceMissing && ( { 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' }, }), }); diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 1e9566e9614..99c6041675a 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -32,7 +32,7 @@ type SetupOptions = { }; export function setupExplore(options?: SetupOptions): { - datasources: { [name: string]: DataSourceApi }; + datasources: { [uid: string]: DataSourceApi }; store: ReturnType; 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 { - 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 { + 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, }; diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index d06e675ff2a..eb67e44f75b 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -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, diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index 70b957d17d6..478708e8ea6 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -72,6 +72,9 @@ function setup(state?: any) { testDatasource: jest.fn(), init: jest.fn(), name: 'default', + getRef() { + return { type: 'default', uid: 'default' }; + }, } ); }, diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index fc066dd1d07..74e2456b9c0 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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, diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 7a8b85aa51f..a21c1307238 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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, + { + name: 'testDs2', + type: 'postgres', + uid: 'ds2', + getRef: () => { + return { type: 'postgres', uid: 'ds2' }; + }, + } as DataSourceApi, + ]; + 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, - { name: 'Postgres2', type: 'postgres' } as DataSourceApi + 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'); }); }); }); diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index bcb3fbfa34b..98d996b7745 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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('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 { - return (dispatch, getState) => { +export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult { + 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 { }; } +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 => { + 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), }; } diff --git a/public/app/plugins/datasource/mixed/MixedDataSource.ts b/public/app/plugins/datasource/mixed/MixedDataSource.ts index a266215149c..b9d3761c273 100644 --- a/public/app/plugins/datasource/mixed/MixedDataSource.ts +++ b/public/app/plugins/datasource/mixed/MixedDataSource.ts @@ -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 { 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; }