Explore: Decouple TimeSrv from Explore (#73559)

This commit is contained in:
Piotr Jamróz
2023-09-04 15:08:52 +02:00
committed by GitHub
parent 710f1325e5
commit fdd384ab56
18 changed files with 67 additions and 50 deletions

View File

@ -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;

View File

@ -263,7 +263,7 @@ export class KeybindingSrv {
const url = await getExploreUrl({
panel,
datasourceSrv: getDatasourceSrv(),
timeSrv: getTimeSrv(),
timeRange: getTimeSrv().timeRange(),
});
if (url) {

View File

@ -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)));
});

View File

@ -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 = {

View File

@ -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() {

View File

@ -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' });
};

View File

@ -110,6 +110,7 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
jest.mock('app/core/core', () => ({
contextSrv: {
hasAccess: () => true,
getValidIntervals: (defaultIntervals: string[]) => defaultIntervals,
},
}));

View File

@ -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}

View File

@ -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()));
});
});
});

View File

@ -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?',

View File

@ -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?',

View File

@ -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();

View File

@ -20,6 +20,7 @@ jest.mock('@grafana/runtime', () => ({
jest.mock('app/core/core', () => ({
contextSrv: {
hasAccess: () => true,
getValidIntervals: (defaultIntervals: string[]) => defaultIntervals,
},
}));

View File

@ -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();

View File

@ -53,6 +53,7 @@ jest.mock('app/core/core', () => ({
hasPermission: () => true,
hasAccess: () => true,
isSignedIn: true,
getValidIntervals: (defaultIntervals: string[]) => defaultIntervals,
},
}));

View File

@ -17,6 +17,7 @@ jest.mock('app/core/core', () => {
contextSrv: {
hasPermission: () => true,
hasAccess: () => true,
getValidIntervals: (defaultIntervals: string[]) => defaultIntervals,
},
};
});

View File

@ -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,
});
});

View File

@ -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) {