Commit e66fc3d4 by Hugo Häggmark Committed by GitHub

ReactPanels: Adds Explore menu item (#20236)

* Fix: Adds Explore menuitem to React Panels
Fixes #19865

* Refactor: Adds CMD|CTRL+click to Explore menu item
parent a499586f
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ComponentType } from 'react'; import { ComponentType } from 'react';
import { PluginMeta, GrafanaPlugin } from './plugin'; import { GrafanaPlugin, PluginMeta } from './plugin';
import { PanelData } from './panel'; import { PanelData } from './panel';
import { LogRowModel } from './logs'; import { LogRowModel } from './logs';
import { AnnotationEvent, TimeSeries, TableData, LoadingState, KeyValue } from './data'; import { AnnotationEvent, KeyValue, LoadingState, TableData, TimeSeries } from './data';
import { DataFrame, DataFrameDTO } from './dataFrame'; import { DataFrame, DataFrameDTO } from './dataFrame';
import { TimeRange, RawTimeRange } from './time'; import { RawTimeRange, TimeRange } from './time';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> { export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
...@@ -253,6 +253,8 @@ export abstract class DataSourceApi< ...@@ -253,6 +253,8 @@ export abstract class DataSourceApi<
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json. * in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
*/ */
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>; annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
interpolateVariablesInQueries?(queries: TQuery[]): TQuery[];
} }
export interface QueryEditorProps< export interface QueryEditorProps<
......
import { ComponentClass, ComponentType } from 'react'; import { ComponentClass, ComponentType } from 'react';
import { DataQueryRequest, DataQueryError } from './datasource'; import { DataQueryError, DataQueryRequest } from './datasource';
import { PluginMeta, GrafanaPlugin } from './plugin'; import { GrafanaPlugin, PluginMeta } from './plugin';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
import { LoadingState } from './data'; import { LoadingState } from './data';
import { DataFrame } from './dataFrame'; import { DataFrame } from './dataFrame';
import { TimeRange, TimeZone, AbsoluteTimeRange } from './time'; import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
...@@ -125,7 +125,7 @@ export interface PanelMenuItem { ...@@ -125,7 +125,7 @@ export interface PanelMenuItem {
type?: 'submenu' | 'divider'; type?: 'submenu' | 'divider';
text?: string; text?: string;
iconClassName?: string; iconClassName?: string;
onClick?: () => void; onClick?: (event: React.MouseEvent<any>) => void;
shortcut?: string; shortcut?: string;
subMenu?: PanelMenuItem[]; subMenu?: PanelMenuItem[];
} }
......
...@@ -5,13 +5,13 @@ import appEvents from 'app/core/app_events'; ...@@ -5,13 +5,13 @@ import appEvents from 'app/core/app_events';
import { getExploreUrl } from 'app/core/utils/explore'; import { getExploreUrl } from 'app/core/utils/explore';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { CoreEvents, AppEventEmitter } from 'app/types'; import { AppEventEmitter, CoreEvents } from 'app/types';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { PanelEvents } from '@grafana/data'; import { PanelEvents } from '@grafana/data';
import 'mousetrap-global-bind'; import 'mousetrap-global-bind';
import { ContextSrv } from './context_srv'; import { ContextSrv } from './context_srv';
import { ILocationService, ITimeoutService, IRootScopeService } from 'angular'; import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { getLocationSrv } from '@grafana/runtime'; import { getLocationSrv } from '@grafana/runtime';
...@@ -224,7 +224,13 @@ export class KeybindingSrv { ...@@ -224,7 +224,13 @@ export class KeybindingSrv {
if (dashboard.meta.focusPanelId) { if (dashboard.meta.focusPanelId) {
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
const datasource = await this.datasourceSrv.get(panel.datasource); const datasource = await this.datasourceSrv.get(panel.datasource);
const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv); const url = await getExploreUrl({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: this.datasourceSrv,
timeSrv: this.timeSrv,
});
const urlWithoutBase = locationUtil.stripBaseFromUrl(url); const urlWithoutBase = locationUtil.stripBaseFromUrl(url);
if (urlWithoutBase) { if (urlWithoutBase) {
......
...@@ -3,34 +3,34 @@ import _ from 'lodash'; ...@@ -3,34 +3,34 @@ import _ from 'lodash';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
// Services & Utils // Services & Utils
import { import {
dateMath,
toUtc,
TimeRange,
RawTimeRange,
TimeZone,
TimeFragment,
LogRowModel,
LogsModel,
LogsDedupStrategy,
IntervalValues,
DefaultTimeZone,
DataQuery, DataQuery,
DataSourceApi,
DataQueryError, DataQueryError,
DataQueryRequest, DataQueryRequest,
PanelModel, DataSourceApi,
dateMath,
DefaultTimeZone,
HistoryItem, HistoryItem,
IntervalValues,
LogRowModel,
LogsDedupStrategy,
LogsModel,
PanelModel,
RawTimeRange,
TimeFragment,
TimeRange,
TimeZone,
toUtc,
} from '@grafana/data'; } from '@grafana/data';
import { renderUrl } from 'app/core/utils/url'; import { renderUrl } from 'app/core/utils/url';
import store from 'app/core/store'; import store from 'app/core/store';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { getNextRefIdChar } from './query'; import { getNextRefIdChar } from './query';
// Types // Types
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import { ExploreUrlState, QueryTransaction, QueryOptions, ExploreMode } from 'app/types/explore'; import { ExploreMode, ExploreUrlState, QueryOptions, QueryTransaction } from 'app/types/explore';
import { config } from '../config'; import { config } from '../config';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DataSourceSrv } from '@grafana/runtime';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-1h', from: 'now-1h',
...@@ -57,13 +57,15 @@ export const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DAT ...@@ -57,13 +57,15 @@ export const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DAT
* @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
* @param timeSrv Time service to get the current dashboard range from * @param timeSrv Time service to get the current dashboard range from
*/ */
export async function getExploreUrl( export interface GetExploreUrlArguments {
panel: PanelModel, panel: PanelModel;
panelTargets: DataQuery[], panelTargets: DataQuery[];
panelDatasource: any, panelDatasource: DataSourceApi;
datasourceSrv: any, datasourceSrv: DataSourceSrv;
timeSrv: TimeSrv timeSrv: TimeSrv;
) { }
export async function getExploreUrl(args: GetExploreUrlArguments) {
const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args;
let exploreDatasource = panelDatasource; let exploreDatasource = panelDatasource;
let exploreTargets: DataQuery[] = panelTargets; let exploreTargets: DataQuery[] = panelTargets;
let url: string; let url: string;
......
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import config from 'app/core/config'; import config from 'app/core/config';
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
import { PanelMenuItem } from '@grafana/data';
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { getLocationSrv } from '@grafana/runtime'; import { contextSrv } from '../../../core/services/context_srv';
import { PanelMenuItem } from '@grafana/data'; import { navigateToExplore } from '../../explore/state/actions';
import { getExploreUrl } from '../../../core/utils/explore';
import { getTimeSrv } from '../services/TimeSrv';
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
const onViewPanel = () => { const onViewPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
store.dispatch( store.dispatch(
updateLocation({ updateLocation({
query: { query: {
...@@ -22,7 +27,8 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ...@@ -22,7 +27,8 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
); );
}; };
const onEditPanel = () => { const onEditPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
store.dispatch( store.dispatch(
updateLocation({ updateLocation({
query: { query: {
...@@ -35,11 +41,13 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ...@@ -35,11 +41,13 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
); );
}; };
const onSharePanel = () => { const onSharePanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
sharePanel(dashboard, panel); sharePanel(dashboard, panel);
}; };
const onInspectPanel = () => { const onInspectPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
getLocationSrv().update({ getLocationSrv().update({
partial: true, partial: true,
query: { query: {
...@@ -48,22 +56,36 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ...@@ -48,22 +56,36 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
}); });
}; };
const onDuplicatePanel = () => { const onMore = (event: React.MouseEvent<any>) => {
event.preventDefault();
};
const onDuplicatePanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
duplicatePanel(dashboard, panel); duplicatePanel(dashboard, panel);
}; };
const onCopyPanel = () => { const onCopyPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
copyPanel(panel); copyPanel(panel);
}; };
const onEditPanelJson = () => { const onEditPanelJson = (event: React.MouseEvent<any>) => {
event.preventDefault();
editPanelJson(dashboard, panel); editPanelJson(dashboard, panel);
}; };
const onRemovePanel = () => { const onRemovePanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
removePanel(dashboard, panel, true); removePanel(dashboard, panel, true);
}; };
const onNavigateToExplore = (event: React.MouseEvent<any>) => {
event.preventDefault();
const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(url) : undefined;
store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }));
};
const menu: PanelMenuItem[] = []; const menu: PanelMenuItem[] = [];
menu.push({ menu.push({
...@@ -89,6 +111,14 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ...@@ -89,6 +111,14 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
shortcut: 'p s', shortcut: 'p s',
}); });
if (contextSrv.hasAccessToExplore() && panel.datasource) {
menu.push({
text: 'Explore',
iconClassName: 'gicon gicon-explore',
shortcut: 'x',
onClick: onNavigateToExplore,
});
}
if (config.featureToggles.inspect) { if (config.featureToggles.inspect) {
menu.push({ menu.push({
text: 'Inspect', text: 'Inspect',
...@@ -123,6 +153,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ...@@ -123,6 +153,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
text: 'More...', text: 'More...',
iconClassName: 'fa fa-fw fa-cube', iconClassName: 'fa fa-fw fa-cube',
subMenu: subMenu, subMenu: subMenu,
onClick: onMore,
}); });
if (dashboard.meta.canEdit) { if (dashboard.meta.canEdit) {
......
import { refreshExplore, loadDatasource } from './actions'; import { loadDatasource, navigateToExplore, refreshExplore } from './actions';
import { ExploreId, ExploreUrlState, ExploreUpdateState, ExploreMode } from 'app/types'; import { ExploreId, ExploreMode, ExploreUpdateState, ExploreUrlState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester'; import { thunkTester } from 'test/core/thunk/thunkTester';
import { import {
initializeExploreAction, initializeExploreAction,
InitializeExplorePayload, InitializeExplorePayload,
updateUIStateAction,
setQueriesAction,
loadDatasourcePendingAction, loadDatasourcePendingAction,
loadDatasourceReadyAction, loadDatasourceReadyAction,
setQueriesAction,
updateUIStateAction,
} from './actionTypes'; } from './actionTypes';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
import { ActionOf } from 'app/core/redux/actionCreatorFactory'; import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { makeInitialUpdateState } from './reducers'; import { makeInitialUpdateState } from './reducers';
import { DataQuery, DefaultTimeZone, RawTimeRange, LogsDedupStrategy, toUtc } from '@grafana/data'; import { DataQuery, DefaultTimeZone, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data';
import { PanelModel } from 'app/features/dashboard/state';
import { updateLocation } from '../../../core/actions';
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
jest.mock('app/features/plugins/datasource_srv', () => ({ jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({ getDatasourceSrv: () => ({
...@@ -218,3 +221,129 @@ describe('loading datasource', () => { ...@@ -218,3 +221,129 @@ describe('loading datasource', () => {
}); });
}); });
}); });
const getNavigateToExploreContext = async (openInNewWindow: (url: string) => void = undefined) => {
const url = 'http://www.someurl.com';
const panel: Partial<PanelModel> = {
datasource: 'mocked datasource',
targets: [{ refId: 'A' }],
};
const datasource = new MockDataSourceApi(panel.datasource);
const get = jest.fn().mockResolvedValue(datasource);
const getDataSourceSrv = jest.fn().mockReturnValue({ get });
const getTimeSrv = jest.fn();
const getExploreUrl = jest.fn().mockResolvedValue(url);
const dispatchedActions = await thunkTester({})
.givenThunk(navigateToExplore)
.whenThunkIsDispatched(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow });
return {
url,
panel,
datasource,
get,
getDataSourceSrv,
getTimeSrv,
getExploreUrl,
dispatchedActions,
};
};
describe('navigateToExplore', () => {
describe('when navigateToExplore thunk is dispatched', () => {
describe('and openInNewWindow is undefined', () => {
const openInNewWindow: (url: string) => void = undefined;
it('then it should dispatch correct actions', async () => {
const { dispatchedActions, url } = await getNavigateToExploreContext(openInNewWindow);
expect(dispatchedActions).toEqual([updateLocation({ path: url, query: {} })]);
});
it('then getDataSourceSrv should have been once', async () => {
const { getDataSourceSrv } = await getNavigateToExploreContext(openInNewWindow);
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
});
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith(panel.datasource);
});
it('then getTimeSrv should have been called once', async () => {
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
expect(getTimeSrv).toHaveBeenCalledTimes(1);
});
it('then getExploreUrl should have been called with correct arguments', async () => {
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
openInNewWindow
);
expect(getExploreUrl).toHaveBeenCalledTimes(1);
expect(getExploreUrl).toHaveBeenCalledWith({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: getDataSourceSrv(),
timeSrv: getTimeSrv(),
});
});
});
describe('and openInNewWindow is defined', () => {
const openInNewWindow: (url: string) => void = jest.fn();
it('then it should dispatch no actions', async () => {
const { dispatchedActions } = await getNavigateToExploreContext(openInNewWindow);
expect(dispatchedActions).toEqual([]);
});
it('then getDataSourceSrv should have been once', async () => {
const { getDataSourceSrv } = await getNavigateToExploreContext(openInNewWindow);
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
});
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith(panel.datasource);
});
it('then getTimeSrv should have been called once', async () => {
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
expect(getTimeSrv).toHaveBeenCalledTimes(1);
});
it('then getExploreUrl should have been called with correct arguments', async () => {
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
openInNewWindow
);
expect(getExploreUrl).toHaveBeenCalledTimes(1);
expect(getExploreUrl).toHaveBeenCalledWith({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: getDataSourceSrv(),
timeSrv: getTimeSrv(),
});
});
it('then openInNewWindow should have been called with correct arguments', async () => {
const openInNewWindowFunc = jest.fn();
const { url } = await getNavigateToExploreContext(openInNewWindowFunc);
expect(openInNewWindowFunc).toHaveBeenCalledTimes(1);
expect(openInNewWindowFunc).toHaveBeenCalledWith(url);
});
});
});
});
...@@ -6,81 +6,84 @@ import store from 'app/core/store'; ...@@ -6,81 +6,84 @@ import store from 'app/core/store';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
import { import {
buildQueryTransaction,
clearQueryKeys,
ensureQueries, ensureQueries,
generateEmptyQuery, generateEmptyQuery,
parseUrlState, generateNewKeyAndAddRefIdIfMissing,
GetExploreUrlArguments,
getTimeRange, getTimeRange,
getTimeRangeFromUrl, getTimeRangeFromUrl,
generateNewKeyAndAddRefIdIfMissing,
lastUsedDatasourceKeyForOrgId,
hasNonEmptyQuery, hasNonEmptyQuery,
buildQueryTransaction, lastUsedDatasourceKeyForOrgId,
clearQueryKeys, parseUrlState,
serializeStateToUrlParam, serializeStateToUrlParam,
stopQueryState, stopQueryState,
updateHistory, updateHistory,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
// Types // Types
import { ThunkResult, ExploreUrlState, ExploreItemState } from 'app/types'; import { ExploreItemState, ExploreUrlState, ThunkResult } from 'app/types';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import { import {
DataSourceApi, AbsoluteTimeRange,
DataQuery, DataQuery,
DataSourceApi,
DataSourceSelectItem, DataSourceSelectItem,
QueryFixAction, dateTimeForTimeZone,
isDateTime,
LoadingState,
LogsDedupStrategy,
PanelData, PanelData,
QueryFixAction,
RawTimeRange, RawTimeRange,
LogsDedupStrategy,
AbsoluteTimeRange,
LoadingState,
TimeRange, TimeRange,
isDateTime,
dateTimeForTimeZone,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreId, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore'; import { ExploreId, ExploreMode, ExploreUIState, QueryOptions } from 'app/types/explore';
import { import {
updateDatasourceInstanceAction, addQueryRowAction,
changeModeAction,
changeQueryAction, changeQueryAction,
changeRangeAction,
changeRefreshIntervalAction, changeRefreshIntervalAction,
ChangeRefreshIntervalPayload, ChangeRefreshIntervalPayload,
changeSizeAction, changeSizeAction,
ChangeSizePayload, ChangeSizePayload,
clearOriginAction,
clearQueriesAction, clearQueriesAction,
historyUpdatedAction,
initializeExploreAction, initializeExploreAction,
loadDatasourceMissingAction, loadDatasourceMissingAction,
loadDatasourcePendingAction, loadDatasourcePendingAction,
queriesImportedAction,
LoadDatasourceReadyPayload,
loadDatasourceReadyAction, loadDatasourceReadyAction,
LoadDatasourceReadyPayload,
loadExploreDatasources,
modifyQueriesAction, modifyQueriesAction,
queriesImportedAction,
queryStoreSubscriptionAction,
queryStreamUpdatedAction,
scanStartAction, scanStartAction,
scanStopAction,
setQueriesAction, setQueriesAction,
setUrlReplacedAction,
splitCloseAction, splitCloseAction,
splitOpenAction, splitOpenAction,
addQueryRowAction, syncTimesAction,
toggleGraphAction, toggleGraphAction,
toggleTableAction,
ToggleGraphPayload, ToggleGraphPayload,
toggleTableAction,
ToggleTablePayload, ToggleTablePayload,
updateDatasourceInstanceAction,
updateUIStateAction, updateUIStateAction,
loadExploreDatasources,
changeModeAction,
scanStopAction,
setUrlReplacedAction,
changeRangeAction,
historyUpdatedAction,
queryStreamUpdatedAction,
queryStoreSubscriptionAction,
clearOriginAction,
syncTimesAction,
} from './actionTypes'; } from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; import { ActionCreator, ActionOf } from 'app/core/redux/actionCreatorFactory';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getShiftedTimeRange } from 'app/core/utils/timePicker';
import { updateLocation } from '../../../core/actions'; import { updateLocation } from '../../../core/actions';
import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { runRequest, preProcessPanelData } from '../../dashboard/state/runRequest'; import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
import { PanelModel } from 'app/features/dashboard/state';
import { DataSourceSrv } from '@grafana/runtime';
/** /**
* Updates UI state and save it to the URL * Updates UI state and save it to the URL
...@@ -764,3 +767,36 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> { ...@@ -764,3 +767,36 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
} }
}; };
} }
export interface NavigateToExploreDependencies {
getDataSourceSrv: () => DataSourceSrv;
getTimeSrv: () => TimeSrv;
getExploreUrl: (args: GetExploreUrlArguments) => Promise<string>;
openInNewWindow?: (url: string) => void;
}
export const navigateToExplore = (
panel: PanelModel,
dependencies: NavigateToExploreDependencies
): ThunkResult<void> => {
return async dispatch => {
const { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow } = dependencies;
const datasourceSrv = getDataSourceSrv();
const datasource = await datasourceSrv.get(panel.datasource);
const path = await getExploreUrl({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv,
timeSrv: getTimeSrv(),
});
if (openInNewWindow) {
openInNewWindow(path);
return;
}
const query = {}; // strips any angular query param
dispatch(updateLocation({ path, query }));
};
};
...@@ -7,16 +7,16 @@ import { getExploreUrl } from 'app/core/utils/explore'; ...@@ -7,16 +7,16 @@ import { getExploreUrl } from 'app/core/utils/explore';
import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel'; import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel';
import { ContextSrv } from 'app/core/services/context_srv'; import { ContextSrv } from 'app/core/services/context_srv';
import { import {
toLegacyResponseData,
toDataFrameDTO,
TimeRange,
LoadingState,
DataFrame, DataFrame,
LegacyResponseData, DataQueryResponse,
DataSourceApi, DataSourceApi,
LegacyResponseData,
LoadingState,
PanelData, PanelData,
DataQueryResponse,
PanelEvents, PanelEvents,
TimeRange,
toDataFrameDTO,
toLegacyResponseData,
} from '@grafana/data'; } from '@grafana/data';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';
...@@ -256,7 +256,13 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -256,7 +256,13 @@ class MetricsPanelCtrl extends PanelCtrl {
text: 'Explore', text: 'Explore',
icon: 'gicon gicon-explore', icon: 'gicon gicon-explore',
shortcut: 'x', shortcut: 'x',
href: await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv), href: await getExploreUrl({
panel: this.panel,
panelTargets: this.panel.targets,
panelDatasource: this.datasource,
datasourceSrv: this.datasourceSrv,
timeSrv: this.timeSrv,
}),
}); });
} }
return items; return items;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment