diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 20b4697d676..70759c92fd7 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -172,6 +172,13 @@ export class ContextSrv { return interval; } + getValidIntervals(intervals: string[]): string[] { + if (this.minRefreshInterval) { + return intervals.filter((str) => str !== '').filter(this.isAllowedInterval); + } + return intervals; + } + hasAccessToExplore() { if (this.accessControlEnabled()) { return this.hasPermission(AccessControlAction.DataSourcesExplore) && config.exploreEnabled; diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 44c30b6a6d8..f2121150f11 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -263,7 +263,7 @@ export class KeybindingSrv { const url = await getExploreUrl({ panel, datasourceSrv: getDatasourceSrv(), - timeSrv: getTimeSrv(), + timeRange: getTimeSrv().timeRange(), }); if (url) { diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index ccd337b8a27..e6e23006f85 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -86,11 +86,11 @@ describe('getExploreUrl', () => { }, getDataSourceById: jest.fn(), }, - timeSrv: { - timeRange: () => ({ raw: { from: 'now-1h', to: 'now' } }), - }, + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1h', to: 'now' } }, } as unknown as GetExploreUrlArguments; - + it('should use raw range in explore url', async () => { + expect(getExploreUrl(args).then((data) => expect(data).toMatch(/from%22:%22now-1h%22,%22to%22:%22now/g))); + }); it('should omit legendFormat in explore url', () => { expect(getExploreUrl(args).then((data) => expect(data).not.toMatch(/legendFormat1/g))); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 438ab5a8298..108530374f9 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -25,7 +25,6 @@ import { import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; import store from 'app/core/store'; -import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { PanelModel } from 'app/features/dashboard/state'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; import { QueryOptions, QueryTransaction } from 'app/types/explore'; @@ -51,8 +50,7 @@ export interface GetExploreUrlArguments { panel: PanelModel; /** Datasource service to query other datasources in case the panel datasource is mixed */ datasourceSrv: DataSourceSrv; - /** Time service to get the current dashboard range from */ - timeSrv: TimeSrv; + timeRange: TimeRange; } export function generateExploreId() { @@ -63,7 +61,7 @@ export function generateExploreId() { * Returns an Explore-URL that contains a panel's queries and the dashboard time range. */ export async function getExploreUrl(args: GetExploreUrlArguments): Promise { - const { panel, datasourceSrv, timeSrv } = args; + const { panel, datasourceSrv, timeRange } = args; let exploreDatasource = await datasourceSrv.get(panel.datasource); /** In Explore, we don't have legend formatter and we don't want to keep @@ -77,8 +75,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise = { range: toURLRange(range) }; + let state: Partial = { range: toURLRange(timeRange.raw) }; if (exploreDatasource.interpolateVariablesInQueries) { const scopedVars = panel.scopedVars || {}; state = { diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 0a1b7fe8268..92e3d162347 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -91,10 +91,7 @@ export class TimeSrv { } getValidIntervals(intervals: string[]): string[] { - if (this.contextSrv.minRefreshInterval) { - return intervals.filter((str) => str !== '').filter(this.contextSrv.isAllowedInterval); - } - return intervals; + return this.contextSrv.getValidIntervals(intervals); } private parseTime() { diff --git a/public/app/features/dashboard/utils/getPanelMenu.ts b/public/app/features/dashboard/utils/getPanelMenu.ts index b9432c5bdf8..bc4634c7d37 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.ts @@ -110,7 +110,14 @@ export function getPanelMenu( event.preventDefault(); const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(`${config.appSubUrl}${url}`) : undefined; - store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }) as any); + store.dispatch( + navigateToExplore(panel, { + getDataSourceSrv, + timeRange: getTimeSrv().timeRange(), + getExploreUrl, + openInNewWindow, + }) as any + ); reportInteraction('dashboards_panelheader_menu', { item: 'explore' }); }; diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 4eea9f1e4b0..1b6b3b9cf4b 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -110,6 +110,7 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { jest.mock('app/core/core', () => ({ contextSrv: { hasAccess: () => true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, }, })); diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 6496f8d15bf..bf1d41aed84 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -11,8 +11,8 @@ import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { StoreState, useDispatch, useSelector } from 'app/types/store'; +import { contextSrv } from '../../core/core'; import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton'; -import { getTimeSrv } from '../dashboard/services/TimeSrv'; import { updateFiscalYearStartMonthForSession, updateTimeZoneForSession } from '../profile/state/reducers'; import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors'; @@ -200,7 +200,7 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props) isLoading={loading} text={showSmallTimePicker ? undefined : loading ? 'Cancel' : 'Run query'} tooltip={showSmallTimePicker ? (loading ? 'Cancel' : 'Run query') : undefined} - intervals={getTimeSrv().getValidIntervals(defaultIntervals)} + intervals={contextSrv.getValidIntervals(defaultIntervals)} isLive={isLive} onRefresh={() => onRunQuery(loading)} noIntervalPicker={isLive} diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index cc8a53189bc..d01a61d406b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'; import { DataQuery, DataSourceRef } from '@grafana/schema'; -import appEvents from 'app/core/app_events'; import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource'; import { RichHistoryQuery } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -14,6 +13,10 @@ import { RichHistoryCard, Props } from './RichHistoryCard'; const starRichHistoryMock = jest.fn(); const deleteRichHistoryMock = jest.fn(); +const mockEventBus = { + publish: jest.fn(), +}; + class MockDatasourceApi implements DataSourceApi { name: string; id: number; @@ -66,6 +69,7 @@ const dsStore: Record = { jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), reportInteraction: jest.fn(), + getAppEvents: () => mockEventBus, })); jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { @@ -486,7 +490,7 @@ describe('RichHistoryCard', () => { const deleteButton = await screen.findByLabelText('Delete query'); await userEvent.click(deleteButton); expect(deleteRichHistoryMock).not.toBeCalled(); - expect(appEvents.publish).toHaveBeenCalledWith(new ShowConfirmModalEvent(expect.anything())); + expect(mockEventBus.publish).toHaveBeenCalledWith(new ShowConfirmModalEvent(expect.anything())); }); }); }); diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index b702926d13b..0f6d22249fb 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -4,11 +4,10 @@ import { connect, ConnectedProps } from 'react-redux'; import { useAsync } from 'react-use'; import { GrafanaTheme2, DataSourceApi } from '@grafana/data'; -import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { config, getDataSourceSrv, reportInteraction, getAppEvents } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { TextArea, Button, IconButton, useStyles2, LoadingPlaceholder } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; -import appEvents from 'app/core/app_events'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { copyStringToClipboard } from 'app/core/utils/explore'; import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory'; @@ -234,7 +233,7 @@ export function RichHistoryCard(props: Props) { // For starred queries, we want confirmation. For non-starred, we don't. if (query.starred) { - appEvents.publish( + getAppEvents().publish( new ShowConfirmModalEvent({ title: 'Delete', text: 'Are you sure you want to permanently delete your starred query?', diff --git a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx index a3df91c7d47..38b9740cd9a 100644 --- a/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; import { useStyles2, Select, Button, Field, InlineField, InlineSwitch, Alert } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; -import appEvents from 'app/core/app_events'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { MAX_HISTORY_ITEMS } from 'app/core/history/RichHistoryLocalStorage'; import { dispatch } from 'app/store/store'; @@ -63,7 +63,7 @@ export function RichHistorySettingsTab(props: RichHistorySettingsProps) { const selectedOption = retentionPeriodOptions.find((v) => v.value === retentionPeriod); const onDelete = () => { - appEvents.publish( + getAppEvents().publish( new ShowConfirmModalEvent({ title: 'Delete', text: 'Are you sure you want to permanently delete your query history?', diff --git a/public/app/features/explore/spec/datasourceState.test.tsx b/public/app/features/explore/spec/datasourceState.test.tsx index aa4d3fef785..69b3a1bf8dc 100644 --- a/public/app/features/explore/spec/datasourceState.test.tsx +++ b/public/app/features/explore/spec/datasourceState.test.tsx @@ -19,6 +19,14 @@ jest.mock('@grafana/runtime', () => ({ getAppEvents: () => testEventBus, })); +jest.mock('app/core/core', () => ({ + contextSrv: { + hasAccess: () => true, + hasPermission: () => true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, + }, +})); + describe('Explore: handle datasource states', () => { afterEach(() => { tearDown(); diff --git a/public/app/features/explore/spec/interpolation.test.tsx b/public/app/features/explore/spec/interpolation.test.tsx index f506d73c039..1b41048b2cf 100644 --- a/public/app/features/explore/spec/interpolation.test.tsx +++ b/public/app/features/explore/spec/interpolation.test.tsx @@ -20,6 +20,7 @@ jest.mock('@grafana/runtime', () => ({ jest.mock('app/core/core', () => ({ contextSrv: { hasAccess: () => true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, }, })); diff --git a/public/app/features/explore/spec/query.test.tsx b/public/app/features/explore/spec/query.test.tsx index 21e657cd381..13d6c670508 100644 --- a/public/app/features/explore/spec/query.test.tsx +++ b/public/app/features/explore/spec/query.test.tsx @@ -18,6 +18,13 @@ jest.mock('../../correlations/utils', () => { }; }); +jest.mock('app/core/core', () => ({ + contextSrv: { + hasAccess: () => true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, + }, +})); + describe('Explore: handle running/not running query', () => { afterEach(() => { tearDown(); diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 83dc22ec080..8cd35ce02c5 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -53,6 +53,7 @@ jest.mock('app/core/core', () => ({ hasPermission: () => true, hasAccess: () => true, isSignedIn: true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, }, })); diff --git a/public/app/features/explore/spec/split.test.tsx b/public/app/features/explore/spec/split.test.tsx index e2f339ecb22..a5706a2392c 100644 --- a/public/app/features/explore/spec/split.test.tsx +++ b/public/app/features/explore/spec/split.test.tsx @@ -17,6 +17,7 @@ jest.mock('app/core/core', () => { contextSrv: { hasPermission: () => true, hasAccess: () => true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, }, }; }); diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index d388986b0dd..a0ab908725d 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -1,6 +1,6 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; -import { ExploreUrlState } from '@grafana/data'; +import { dateTime, ExploreUrlState } from '@grafana/data'; import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { locationService } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state'; @@ -21,19 +21,19 @@ const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => vo const datasource = new MockDataSourceApi(panel.datasource!.uid!); const get = jest.fn().mockResolvedValue(datasource); const getDataSourceSrv = jest.fn().mockReturnValue({ get }); - const getTimeSrv = jest.fn(); const getExploreUrl = jest.fn().mockResolvedValue(url); + const timeRange = { from: dateTime(), to: dateTime() }; const dispatchedActions = await thunkTester({}) .givenThunk(navigateToExplore) - .whenThunkIsDispatched(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }); + .whenThunkIsDispatched(panel, { getDataSourceSrv, timeRange, getExploreUrl, openInNewWindow }); return { url, panel, get, getDataSourceSrv, - getTimeSrv, + timeRange, getExploreUrl, dispatchedActions, }; @@ -53,20 +53,14 @@ describe('navigateToExplore', () => { expect(getDataSourceSrv).toHaveBeenCalledTimes(1); }); - it('then getTimeSrv should have been called once', async () => { - const { getTimeSrv } = await getNavigateToExploreContext(); - - expect(getTimeSrv).toHaveBeenCalledTimes(1); - }); - it('then getExploreUrl should have been called with correct arguments', async () => { - const { getExploreUrl, panel, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(); + const { getExploreUrl, panel, getDataSourceSrv, timeRange } = await getNavigateToExploreContext(); expect(getExploreUrl).toHaveBeenCalledTimes(1); expect(getExploreUrl).toHaveBeenCalledWith({ panel, datasourceSrv: getDataSourceSrv(), - timeSrv: getTimeSrv(), + timeRange, }); }); }); @@ -85,14 +79,8 @@ describe('navigateToExplore', () => { expect(getDataSourceSrv).toHaveBeenCalledTimes(1); }); - 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, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext( + const { getExploreUrl, panel, getDataSourceSrv, timeRange } = await getNavigateToExploreContext( openInNewWindow ); @@ -100,7 +88,7 @@ describe('navigateToExplore', () => { expect(getExploreUrl).toHaveBeenCalledWith({ panel, datasourceSrv: getDataSourceSrv(), - timeSrv: getTimeSrv(), + timeRange, }); }); diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 21d7f9d744b..7804fd7714e 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { AnyAction } from 'redux'; -import { SplitOpenOptions } from '@grafana/data'; +import { SplitOpenOptions, TimeRange } from '@grafana/data'; import { DataSourceSrv, locationService } from '@grafana/runtime'; import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explore'; import { PanelModel } from 'app/features/dashboard/state'; @@ -10,7 +10,6 @@ import { ExploreItemState, ExploreState } from 'app/types/explore'; import { RichHistoryResults } from '../../../core/history/RichHistoryStorage'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; import { createAsyncThunk, ThunkResult } from '../../../types'; -import { TimeSrv } from '../../dashboard/services/TimeSrv'; import { withUniqueRefIds } from '../utils/queries'; import { initializeExplore, InitializeExploreOptions, paneReducer } from './explorePane'; @@ -107,7 +106,7 @@ const createNewSplitOpenPane = createAsyncThunk( export interface NavigateToExploreDependencies { getDataSourceSrv: () => DataSourceSrv; - getTimeSrv: () => TimeSrv; + timeRange: TimeRange; getExploreUrl: (args: GetExploreUrlArguments) => Promise; openInNewWindow?: (url: string) => void; } @@ -117,12 +116,12 @@ export const navigateToExplore = ( dependencies: NavigateToExploreDependencies ): ThunkResult => { return async (dispatch) => { - const { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow } = dependencies; + const { getDataSourceSrv, timeRange, getExploreUrl, openInNewWindow } = dependencies; const datasourceSrv = getDataSourceSrv(); const path = await getExploreUrl({ panel, datasourceSrv, - timeSrv: getTimeSrv(), + timeRange, }); if (openInNewWindow && path) {