mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 21:52:43 +08: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