mirror of
				https://github.com/grafana/grafana.git
				synced 2025-02-25 18:55:37 -06:00 
			
		
		
		
	Explore: Decouple TimeSrv from Explore (#73559)
This commit is contained in:
		| @@ -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; | ||||
|   | ||||
| @@ -263,7 +263,7 @@ export class KeybindingSrv { | ||||
|         const url = await getExploreUrl({ | ||||
|           panel, | ||||
|           datasourceSrv: getDatasourceSrv(), | ||||
|           timeSrv: getTimeSrv(), | ||||
|           timeRange: getTimeSrv().timeRange(), | ||||
|         }); | ||||
|  | ||||
|         if (url) { | ||||
|   | ||||
| @@ -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))); | ||||
|   }); | ||||
|   | ||||
| @@ -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<string | undefined> { | ||||
|   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<strin | ||||
|   let url: string | undefined; | ||||
|  | ||||
|   if (exploreDatasource) { | ||||
|     const range = timeSrv.timeRange().raw; | ||||
|     let state: Partial<ExploreUrlState> = { range: toURLRange(range) }; | ||||
|     let state: Partial<ExploreUrlState> = { range: toURLRange(timeRange.raw) }; | ||||
|     if (exploreDatasource.interpolateVariablesInQueries) { | ||||
|       const scopedVars = panel.scopedVars || {}; | ||||
|       state = { | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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' }); | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -110,6 +110,7 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { | ||||
| jest.mock('app/core/core', () => ({ | ||||
|   contextSrv: { | ||||
|     hasAccess: () => true, | ||||
|     getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, | ||||
|   }, | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -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} | ||||
|   | ||||
| @@ -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<T extends DataQuery> implements DataSourceApi<T> { | ||||
|   name: string; | ||||
|   id: number; | ||||
| @@ -66,6 +69,7 @@ const dsStore: Record<string, DataSourceApi> = { | ||||
| 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())); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -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?', | ||||
|   | ||||
| @@ -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?', | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -20,6 +20,7 @@ jest.mock('@grafana/runtime', () => ({ | ||||
| jest.mock('app/core/core', () => ({ | ||||
|   contextSrv: { | ||||
|     hasAccess: () => true, | ||||
|     getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, | ||||
|   }, | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -53,6 +53,7 @@ jest.mock('app/core/core', () => ({ | ||||
|     hasPermission: () => true, | ||||
|     hasAccess: () => true, | ||||
|     isSignedIn: true, | ||||
|     getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, | ||||
|   }, | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,7 @@ jest.mock('app/core/core', () => { | ||||
|     contextSrv: { | ||||
|       hasPermission: () => true, | ||||
|       hasAccess: () => true, | ||||
|       getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, | ||||
|     }, | ||||
|   }; | ||||
| }); | ||||
|   | ||||
| @@ -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, | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|   | ||||
| @@ -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<string | undefined>; | ||||
|   openInNewWindow?: (url: string) => void; | ||||
| } | ||||
| @@ -117,12 +116,12 @@ export const navigateToExplore = ( | ||||
|   dependencies: NavigateToExploreDependencies | ||||
| ): ThunkResult<void> => { | ||||
|   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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user