mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ReactPanels: Adds Explore menu item (#20236)
* Fix: Adds Explore menuitem to React Panels Fixes #19865 * Refactor: Adds CMD|CTRL+click to Explore menu item
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { store } from 'app/store/store';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
|
||||
import { PanelMenuItem } from '@grafana/data';
|
||||
|
||||
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { contextSrv } from '../../../core/services/context_srv';
|
||||
import { navigateToExplore } from '../../explore/state/actions';
|
||||
import { getExploreUrl } from '../../../core/utils/explore';
|
||||
import { getTimeSrv } from '../services/TimeSrv';
|
||||
|
||||
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
const onViewPanel = () => {
|
||||
const onViewPanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
@@ -22,7 +27,8 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onEditPanel = () => {
|
||||
const onEditPanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
@@ -35,11 +41,13 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSharePanel = () => {
|
||||
const onSharePanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
sharePanel(dashboard, panel);
|
||||
};
|
||||
|
||||
const onInspectPanel = () => {
|
||||
const onInspectPanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
getLocationSrv().update({
|
||||
partial: true,
|
||||
query: {
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
const onCopyPanel = () => {
|
||||
const onCopyPanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
copyPanel(panel);
|
||||
};
|
||||
|
||||
const onEditPanelJson = () => {
|
||||
const onEditPanelJson = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
editPanelJson(dashboard, panel);
|
||||
};
|
||||
|
||||
const onRemovePanel = () => {
|
||||
const onRemovePanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
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[] = [];
|
||||
|
||||
menu.push({
|
||||
@@ -89,6 +111,14 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
shortcut: 'p s',
|
||||
});
|
||||
|
||||
if (contextSrv.hasAccessToExplore() && panel.datasource) {
|
||||
menu.push({
|
||||
text: 'Explore',
|
||||
iconClassName: 'gicon gicon-explore',
|
||||
shortcut: 'x',
|
||||
onClick: onNavigateToExplore,
|
||||
});
|
||||
}
|
||||
if (config.featureToggles.inspect) {
|
||||
menu.push({
|
||||
text: 'Inspect',
|
||||
@@ -123,6 +153,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
text: 'More...',
|
||||
iconClassName: 'fa fa-fw fa-cube',
|
||||
subMenu: subMenu,
|
||||
onClick: onMore,
|
||||
});
|
||||
|
||||
if (dashboard.meta.canEdit) {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { refreshExplore, loadDatasource } from './actions';
|
||||
import { ExploreId, ExploreUrlState, ExploreUpdateState, ExploreMode } from 'app/types';
|
||||
import { loadDatasource, navigateToExplore, refreshExplore } from './actions';
|
||||
import { ExploreId, ExploreMode, ExploreUpdateState, ExploreUrlState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import {
|
||||
initializeExploreAction,
|
||||
InitializeExplorePayload,
|
||||
updateUIStateAction,
|
||||
setQueriesAction,
|
||||
loadDatasourcePendingAction,
|
||||
loadDatasourceReadyAction,
|
||||
setQueriesAction,
|
||||
updateUIStateAction,
|
||||
} from './actionTypes';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
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', () => ({
|
||||
getDatasourceSrv: () => ({
|
||||
@@ -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';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import {
|
||||
ensureQueries,
|
||||
generateEmptyQuery,
|
||||
parseUrlState,
|
||||
getTimeRange,
|
||||
getTimeRangeFromUrl,
|
||||
generateNewKeyAndAddRefIdIfMissing,
|
||||
lastUsedDatasourceKeyForOrgId,
|
||||
hasNonEmptyQuery,
|
||||
buildQueryTransaction,
|
||||
clearQueryKeys,
|
||||
ensureQueries,
|
||||
generateEmptyQuery,
|
||||
generateNewKeyAndAddRefIdIfMissing,
|
||||
GetExploreUrlArguments,
|
||||
getTimeRange,
|
||||
getTimeRangeFromUrl,
|
||||
hasNonEmptyQuery,
|
||||
lastUsedDatasourceKeyForOrgId,
|
||||
parseUrlState,
|
||||
serializeStateToUrlParam,
|
||||
stopQueryState,
|
||||
updateHistory,
|
||||
} from 'app/core/utils/explore';
|
||||
// Types
|
||||
import { ThunkResult, ExploreUrlState, ExploreItemState } from 'app/types';
|
||||
import { ExploreItemState, ExploreUrlState, ThunkResult } from 'app/types';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataQuery,
|
||||
DataSourceSelectItem,
|
||||
QueryFixAction,
|
||||
PanelData,
|
||||
RawTimeRange,
|
||||
LogsDedupStrategy,
|
||||
AbsoluteTimeRange,
|
||||
LoadingState,
|
||||
TimeRange,
|
||||
isDateTime,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
DataSourceSelectItem,
|
||||
dateTimeForTimeZone,
|
||||
isDateTime,
|
||||
LoadingState,
|
||||
LogsDedupStrategy,
|
||||
PanelData,
|
||||
QueryFixAction,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore';
|
||||
import { ExploreId, ExploreMode, ExploreUIState, QueryOptions } from 'app/types/explore';
|
||||
import {
|
||||
updateDatasourceInstanceAction,
|
||||
addQueryRowAction,
|
||||
changeModeAction,
|
||||
changeQueryAction,
|
||||
changeRangeAction,
|
||||
changeRefreshIntervalAction,
|
||||
ChangeRefreshIntervalPayload,
|
||||
changeSizeAction,
|
||||
ChangeSizePayload,
|
||||
clearOriginAction,
|
||||
clearQueriesAction,
|
||||
historyUpdatedAction,
|
||||
initializeExploreAction,
|
||||
loadDatasourceMissingAction,
|
||||
loadDatasourcePendingAction,
|
||||
queriesImportedAction,
|
||||
LoadDatasourceReadyPayload,
|
||||
loadDatasourceReadyAction,
|
||||
LoadDatasourceReadyPayload,
|
||||
loadExploreDatasources,
|
||||
modifyQueriesAction,
|
||||
queriesImportedAction,
|
||||
queryStoreSubscriptionAction,
|
||||
queryStreamUpdatedAction,
|
||||
scanStartAction,
|
||||
scanStopAction,
|
||||
setQueriesAction,
|
||||
setUrlReplacedAction,
|
||||
splitCloseAction,
|
||||
splitOpenAction,
|
||||
addQueryRowAction,
|
||||
toggleGraphAction,
|
||||
toggleTableAction,
|
||||
ToggleGraphPayload,
|
||||
ToggleTablePayload,
|
||||
updateUIStateAction,
|
||||
loadExploreDatasources,
|
||||
changeModeAction,
|
||||
scanStopAction,
|
||||
setUrlReplacedAction,
|
||||
changeRangeAction,
|
||||
historyUpdatedAction,
|
||||
queryStreamUpdatedAction,
|
||||
queryStoreSubscriptionAction,
|
||||
clearOriginAction,
|
||||
syncTimesAction,
|
||||
toggleGraphAction,
|
||||
ToggleGraphPayload,
|
||||
toggleTableAction,
|
||||
ToggleTablePayload,
|
||||
updateDatasourceInstanceAction,
|
||||
updateUIStateAction,
|
||||
} 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 { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { runRequest, preProcessPanelData } from '../../dashboard/state/runRequest';
|
||||
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
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
|
||||
@@ -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';
|
||||
import { applyPanelTimeOverrides, getResolution } from 'app/features/dashboard/utils/panel';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import {
|
||||
toLegacyResponseData,
|
||||
toDataFrameDTO,
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
DataFrame,
|
||||
LegacyResponseData,
|
||||
DataSourceApi,
|
||||
PanelData,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
LegacyResponseData,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelEvents,
|
||||
TimeRange,
|
||||
toDataFrameDTO,
|
||||
toLegacyResponseData,
|
||||
} from '@grafana/data';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
@@ -256,7 +256,13 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
text: 'Explore',
|
||||
icon: 'gicon gicon-explore',
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user