mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
This reverts commit e2258120e7
.
This commit is contained in:
@@ -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
|
||||||
|
@@ -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.
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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",
|
||||||
|
@@ -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"
|
||||||
|
@@ -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}`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 = () => {
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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}
|
||||||
|
@@ -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' },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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,
|
||||||
|
@@ -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' };
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -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,
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user