Revert "Explore: Add Mixed Datasource (#51605)" (#52889)

This reverts commit e2258120e7.
This commit is contained in:
Kristina
2022-07-27 10:17:31 -05:00
committed by GitHub
parent a4355bd6e4
commit 6ecc420534
18 changed files with 87 additions and 194 deletions

View File

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

View File

@@ -73,7 +73,3 @@ The Share shortened link capability allows you to create smaller and simpler URL
> **Note:** Available in Grafana 8.5.0 and later versions. > **Note:** Available in Grafana 8.5.0 and later versions.
Enabled by default, allows users to create panels in dashboards from within Explore. 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,7 +44,6 @@ export interface FeatureToggles {
export?: boolean; export?: boolean;
azureMonitorResourcePickerForMetrics?: boolean; azureMonitorResourcePickerForMetrics?: boolean;
explore2Dashboard?: boolean; explore2Dashboard?: boolean;
exploreMixedDatasource?: boolean;
tracing?: boolean; tracing?: boolean;
commandPalette?: boolean; commandPalette?: boolean;
cloudWatchDynamicLabels?: boolean; cloudWatchDynamicLabels?: boolean;

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import {
DataQuery, DataQuery,
DataQueryRequest, DataQueryRequest,
DataSourceApi, DataSourceApi,
DataSourceRef,
dateMath, dateMath,
DateTime, DateTime,
DefaultTimeZone, DefaultTimeZone,
@@ -25,7 +24,7 @@ import {
toUtc, toUtc,
urlUtil, urlUtil,
} from '@grafana/data'; } from '@grafana/data';
import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime'; import { DataSourceSrv } from '@grafana/runtime';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import store from 'app/core/store'; import store from 'app/core/store';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -71,6 +70,19 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
let exploreTargets: DataQuery[] = panel.targets.map((t) => omit(t, 'legendFormat')); let exploreTargets: DataQuery[] = panel.targets.map((t) => omit(t, 'legendFormat'));
let url: string | undefined; 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) { if (exploreDatasource) {
const range = timeSrv.timeRangeForUrl(); const range = timeSrv.timeRangeForUrl();
let state: Partial<ExploreUrlState> = { range }; let state: Partial<ExploreUrlState> = { range };
@@ -87,7 +99,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
...state, ...state,
datasource: exploreDatasource.name, datasource: exploreDatasource.name,
context: 'explore', context: 'explore',
queries: exploreTargets, queries: exploreTargets.map((t) => ({ ...t, datasource: exploreDatasource.getRef() })),
}; };
} }
@@ -242,34 +254,8 @@ export function generateKey(index = 0): string {
return `Q-${uuidv4()}-${index}`; return `Q-${uuidv4()}-${index}`;
} }
export async function generateEmptyQuery( export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
queries: DataQuery[], return { refId: getNextRefIdChar(queries), key: generateKey(index) };
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 => { export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => {
@@ -281,13 +267,8 @@ export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: D
/** /**
* Ensure at least one target exists and that targets have the necessary keys * 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 async function ensureQueries( export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
queries?: DataQuery[],
newQueryDataSourceOverride?: DataSourceRef
): Promise<DataQuery[]> {
if (queries && typeof queries === 'object' && queries.length > 0) { if (queries && typeof queries === 'object' && queries.length > 0) {
const allQueries = []; const allQueries = [];
for (let index = 0; index < queries.length; index++) { for (let index = 0; index < queries.length; index++) {
@@ -306,18 +287,7 @@ export async function ensureQueries(
} }
return allQueries; 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 [];
}
} }
/** /**
@@ -374,9 +344,9 @@ export function clearHistory(datasourceId: string) {
store.delete(historyKey); store.delete(historyKey);
} }
export const getQueryKeys = (queries: DataQuery[]): string[] => { export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSourceApi | null): string[] => {
const queryKeys = queries.reduce<string[]>((newQueryKeys, query, index) => { const queryKeys = queries.reduce<string[]>((newQueryKeys, query, index) => {
const primaryKey = query.datasource?.uid || query.key; const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
return newQueryKeys.concat(`${primaryKey}-${index}`); return newQueryKeys.concat(`${primaryKey}-${index}`);
}, []); }, []);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,9 +72,6 @@ function setup(state?: any) {
testDatasource: jest.fn(), testDatasource: jest.fn(),
init: jest.fn(), init: jest.fn(),
name: 'default', 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 // commit changes based on the diff of new url vs old url
if (update.datasource) { if (update.datasource) {
const initialQueries = await ensureQueries(queries); const initialQueries = ensureQueries(queries);
await dispatch( await dispatch(
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState) initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, panelsState)
); );
@@ -304,7 +304,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
range, range,
queries, queries,
initialized: true, initialized: true,
queryKeys: getQueryKeys(queries), queryKeys: getQueryKeys(queries, datasourceInstance),
datasourceInstance, datasourceInstance,
history, history,
datasourceMissing: !datasourceInstance, datasourceMissing: !datasourceInstance,

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { cloneDeep, groupBy, omit } from 'lodash'; import { cloneDeep, groupBy } from 'lodash';
import { forkJoin, from, Observable, of, OperatorFunction } from 'rxjs'; import { forkJoin, from, Observable, of, OperatorFunction } from 'rxjs';
import { catchError, map, mergeAll, mergeMap, reduce, toArray } from 'rxjs/operators'; import { catchError, map, mergeAll, mergeMap, reduce, toArray } from 'rxjs/operators';
@@ -98,13 +98,6 @@ export class MixedDatasource extends DataSourceApi<DataQuery> {
return Promise.resolve({}); 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 { private isQueryable(query: BatchedQueries): boolean {
return query && Array.isArray(query.targets) && query.targets.length > 0; return query && Array.isArray(query.targets) && query.targets.length > 0;
} }