From e66fc3d47ad72807ccf3841195b7398be003f10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 7 Nov 2019 13:49:45 +0100 Subject: [PATCH] ReactPanels: Adds Explore menu item (#20236) * Fix: Adds Explore menuitem to React Panels Fixes #19865 * Refactor: Adds CMD|CTRL+click to Explore menu item --- packages/grafana-data/src/types/datasource.ts | 8 +- packages/grafana-data/src/types/panel.ts | 8 +- public/app/core/services/keybindingSrv.ts | 12 +- public/app/core/utils/explore.ts | 46 +++--- .../features/dashboard/utils/getPanelMenu.ts | 57 +++++-- .../features/explore/state/actions.test.ts | 139 +++++++++++++++++- public/app/features/explore/state/actions.ts | 118 +++++++++------ .../app/features/panel/metrics_panel_ctrl.ts | 22 ++- 8 files changed, 311 insertions(+), 99 deletions(-) diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 320db61c0de..c9ac7b2b831 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -1,11 +1,11 @@ import { Observable } from 'rxjs'; import { ComponentType } from 'react'; -import { PluginMeta, GrafanaPlugin } from './plugin'; +import { GrafanaPlugin, PluginMeta } from './plugin'; import { PanelData } from './panel'; 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 { TimeRange, RawTimeRange } from './time'; +import { RawTimeRange, TimeRange } from './time'; import { ScopedVars } from './ScopedVars'; export interface DataSourcePluginOptionsEditorProps { @@ -253,6 +253,8 @@ export abstract class DataSourceApi< * in the annotation editor `annotations` capability also needs to be enabled in plugin.json. */ annotationQuery?(options: AnnotationQueryRequest): Promise; + + interpolateVariablesInQueries?(queries: TQuery[]): TQuery[]; } export interface QueryEditorProps< diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 6135c9edf66..7e5df1e6307 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -1,10 +1,10 @@ import { ComponentClass, ComponentType } from 'react'; -import { DataQueryRequest, DataQueryError } from './datasource'; -import { PluginMeta, GrafanaPlugin } from './plugin'; +import { DataQueryError, DataQueryRequest } from './datasource'; +import { GrafanaPlugin, PluginMeta } from './plugin'; import { ScopedVars } from './ScopedVars'; import { LoadingState } from './data'; 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; @@ -125,7 +125,7 @@ export interface PanelMenuItem { type?: 'submenu' | 'divider'; text?: string; iconClassName?: string; - onClick?: () => void; + onClick?: (event: React.MouseEvent) => void; shortcut?: string; subMenu?: PanelMenuItem[]; } diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 5c144ab02e2..fb48d33d8f2 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -5,13 +5,13 @@ import appEvents from 'app/core/app_events'; import { getExploreUrl } from 'app/core/utils/explore'; import locationUtil from 'app/core/utils/location_util'; import { store } from 'app/store/store'; -import { CoreEvents, AppEventEmitter } from 'app/types'; +import { AppEventEmitter, CoreEvents } from 'app/types'; import Mousetrap from 'mousetrap'; import { PanelEvents } from '@grafana/data'; import 'mousetrap-global-bind'; import { ContextSrv } from './context_srv'; -import { ILocationService, ITimeoutService, IRootScopeService } from 'angular'; +import { ILocationService, IRootScopeService, ITimeoutService } from 'angular'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { getLocationSrv } from '@grafana/runtime'; @@ -224,7 +224,13 @@ export class KeybindingSrv { if (dashboard.meta.focusPanelId) { const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); 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); if (urlWithoutBase) { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2f88258b158..c40a63b3ec5 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -3,34 +3,34 @@ import _ from 'lodash'; import { Unsubscribable } from 'rxjs'; // Services & Utils import { - dateMath, - toUtc, - TimeRange, - RawTimeRange, - TimeZone, - TimeFragment, - LogRowModel, - LogsModel, - LogsDedupStrategy, - IntervalValues, - DefaultTimeZone, DataQuery, - DataSourceApi, DataQueryError, DataQueryRequest, - PanelModel, + DataSourceApi, + dateMath, + DefaultTimeZone, HistoryItem, + IntervalValues, + LogRowModel, + LogsDedupStrategy, + LogsModel, + PanelModel, + RawTimeRange, + TimeFragment, + TimeRange, + TimeZone, + toUtc, } from '@grafana/data'; import { renderUrl } from 'app/core/utils/url'; import store from 'app/core/store'; import kbn from 'app/core/utils/kbn'; import { getNextRefIdChar } from './query'; - // Types 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 { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { DataSourceSrv } from '@grafana/runtime'; export const DEFAULT_RANGE = { from: 'now-1h', @@ -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 timeSrv Time service to get the current dashboard range from */ -export async function getExploreUrl( - panel: PanelModel, - panelTargets: DataQuery[], - panelDatasource: any, - datasourceSrv: any, - timeSrv: TimeSrv -) { +export interface GetExploreUrlArguments { + panel: PanelModel; + panelTargets: DataQuery[]; + panelDatasource: DataSourceApi; + datasourceSrv: DataSourceSrv; + timeSrv: TimeSrv; +} +export async function getExploreUrl(args: GetExploreUrlArguments) { + const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args; let exploreDatasource = panelDatasource; let exploreTargets: DataQuery[] = panelTargets; let url: string; diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index 53e0ea72a57..7cebf40b1ac 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -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) => { + event.preventDefault(); store.dispatch( updateLocation({ query: { @@ -22,7 +27,8 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ); }; - const onEditPanel = () => { + const onEditPanel = (event: React.MouseEvent) => { + event.preventDefault(); store.dispatch( updateLocation({ query: { @@ -35,11 +41,13 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { ); }; - const onSharePanel = () => { + const onSharePanel = (event: React.MouseEvent) => { + event.preventDefault(); sharePanel(dashboard, panel); }; - const onInspectPanel = () => { + const onInspectPanel = (event: React.MouseEvent) => { + 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) => { + event.preventDefault(); + }; + + const onDuplicatePanel = (event: React.MouseEvent) => { + event.preventDefault(); duplicatePanel(dashboard, panel); }; - const onCopyPanel = () => { + const onCopyPanel = (event: React.MouseEvent) => { + event.preventDefault(); copyPanel(panel); }; - const onEditPanelJson = () => { + const onEditPanelJson = (event: React.MouseEvent) => { + event.preventDefault(); editPanelJson(dashboard, panel); }; - const onRemovePanel = () => { + const onRemovePanel = (event: React.MouseEvent) => { + event.preventDefault(); removePanel(dashboard, panel, true); }; + const onNavigateToExplore = (event: React.MouseEvent) => { + 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) { diff --git a/public/app/features/explore/state/actions.test.ts b/public/app/features/explore/state/actions.test.ts index d1625c513f3..ce431ec3e15 100644 --- a/public/app/features/explore/state/actions.test.ts +++ b/public/app/features/explore/state/actions.test.ts @@ -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 = { + 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); + }); + }); + }); +}); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 49b07b9f9c0..1a6e0e85a3b 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -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 { } }; } + +export interface NavigateToExploreDependencies { + getDataSourceSrv: () => DataSourceSrv; + getTimeSrv: () => TimeSrv; + getExploreUrl: (args: GetExploreUrlArguments) => Promise; + openInNewWindow?: (url: string) => void; +} + +export const navigateToExplore = ( + panel: PanelModel, + dependencies: NavigateToExploreDependencies +): ThunkResult => { + 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 })); + }; +}; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index f0e705124e6..de0e5685a03 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -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;