Explore: Improve handling time range keyboard shortcuts inside Explore (#73600)

* Handle time keyboard shortcuts inside Explore

* Remove unused handler for make absolute

* Remove unused code

* Unify handling keyboard shorcuts logic
This commit is contained in:
Piotr Jamróz 2023-08-23 16:02:49 +02:00 committed by GitHub
parent 8b7566c299
commit f11cc0e60e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 243 additions and 83 deletions

View File

@ -3,7 +3,7 @@ import React from 'react';
import { AutoSizerProps } from 'react-virtualized-auto-sizer'; import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { TestProvider } from 'test/helpers/TestProvider'; import { TestProvider } from 'test/helpers/TestProvider';
import { DataSourceApi, LoadingState, CoreApp, createTheme, EventBusSrv, PluginExtensionTypes } from '@grafana/data'; import { CoreApp, createTheme, DataSourceApi, EventBusSrv, LoadingState, PluginExtensionTypes } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { getPluginLinkExtensions } from '@grafana/runtime'; import { getPluginLinkExtensions } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
@ -72,7 +72,6 @@ const dummyProps: Props = {
isLive: false, isLive: false,
syncedTimes: false, syncedTimes: false,
updateTimeRange: jest.fn(), updateTimeRange: jest.fn(),
makeAbsoluteTime: jest.fn(),
graphResult: [], graphResult: [],
absoluteRange: { absoluteRange: {
from: 0, from: 0,

View File

@ -4,37 +4,34 @@ import memoizeOne from 'memoize-one';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { Unsubscribable } from 'rxjs';
import { import {
AbsoluteTimeRange, AbsoluteTimeRange,
EventBus,
GrafanaTheme2, GrafanaTheme2,
hasToggleableQueryFiltersSupport,
LoadingState, LoadingState,
QueryFixAction, QueryFixAction,
RawTimeRange, RawTimeRange,
EventBus,
SplitOpenOptions, SplitOpenOptions,
SupplementaryQueryType, SupplementaryQueryType,
hasToggleableQueryFiltersSupport,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { import {
AdHocFilterItem,
CustomScrollbar, CustomScrollbar,
ErrorBoundaryAlert, ErrorBoundaryAlert,
PanelContainer,
Themeable2, Themeable2,
withTheme2, withTheme2,
PanelContainer,
AdHocFilterItem,
} from '@grafana/ui'; } from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types';
import appEvents from 'app/core/app_events';
import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider'; import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils'; import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { AbsoluteTimeEvent } from 'app/types/events';
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
@ -67,7 +64,7 @@ import {
setSupplementaryQueryEnabled, setSupplementaryQueryEnabled,
} from './state/query'; } from './state/query';
import { isSplit } from './state/selectors'; import { isSplit } from './state/selectors';
import { makeAbsoluteTime, updateTimeRange } from './state/time'; import { updateTimeRange } from './state/time';
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
@ -141,10 +138,10 @@ export type Props = ExploreProps & ConnectedProps<typeof connector>;
*/ */
export class Explore extends React.PureComponent<Props, ExploreState> { export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined; scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfViewRef = createRef<HTMLDivElement>(); topOfViewRef = createRef<HTMLDivElement>();
graphEventBus: EventBus; graphEventBus: EventBus;
logsEventBus: EventBus; logsEventBus: EventBus;
memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames);
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -155,14 +152,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false }); this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false });
} }
componentDidMount() {
this.absoluteTimeUnsubsciber = appEvents.subscribe(AbsoluteTimeEvent, this.onMakeAbsoluteTime);
}
componentWillUnmount() {
this.absoluteTimeUnsubsciber?.unsubscribe();
}
onChangeTime = (rawRange: RawTimeRange) => { onChangeTime = (rawRange: RawTimeRange) => {
const { updateTimeRange, exploreId } = this.props; const { updateTimeRange, exploreId } = this.props;
updateTimeRange({ exploreId, rawRange }); updateTimeRange({ exploreId, rawRange });
@ -228,11 +217,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
this.props.addQueryRow(exploreId, queryKeys.length); this.props.addQueryRow(exploreId, queryKeys.length);
}; };
onMakeAbsoluteTime = () => {
const { makeAbsoluteTime } = this.props;
makeAbsoluteTime();
};
/** /**
* Used by Logs details. * Used by Logs details.
*/ */
@ -450,8 +434,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
); );
} }
memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames);
renderFlameGraphPanel() { renderFlameGraphPanel() {
const { queryResponse } = this.props; const { queryResponse } = this.props;
return <FlameGraphExploreContainer dataFrames={queryResponse.flameGraphFrames} />; return <FlameGraphExploreContainer dataFrames={queryResponse.flameGraphFrames} />;
@ -660,7 +642,6 @@ const mapDispatchToProps = {
scanStopAction, scanStopAction,
setQueries, setQueries,
updateTimeRange, updateTimeRange,
makeAbsoluteTime,
addQueryRow, addQueryRow,
splitOpen, splitOpen,
setSupplementaryQueryEnabled, setSupplementaryQueryEnabled,

View File

@ -12,6 +12,7 @@ import { ExploreQueryParams } from 'app/types/explore';
import { ExploreActions } from './ExploreActions'; import { ExploreActions } from './ExploreActions';
import { ExplorePaneContainer } from './ExplorePaneContainer'; import { ExplorePaneContainer } from './ExplorePaneContainer';
import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useExplorePageTitle } from './hooks/useExplorePageTitle';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater'; import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater';
import { useStateSync } from './hooks/useStateSync'; import { useStateSync } from './hooks/useStateSync';
import { useTimeSrvFix } from './hooks/useTimeSrvFix'; import { useTimeSrvFix } from './hooks/useTimeSrvFix';
@ -38,7 +39,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
// if we were to update the URL on state change, the title would not match the URL. // if we were to update the URL on state change, the title would not match the URL.
// Ultimately the URL is the single source of truth from which state is derived, the page title is not different // Ultimately the URL is the single source of truth from which state is derived, the page title is not different
useExplorePageTitle(props.queryParams); useExplorePageTitle(props.queryParams);
const { keybindings, chrome } = useGrafana(); const { chrome } = useGrafana();
const navModel = useNavModel('explore'); const navModel = useNavModel('explore');
const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH); const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH);
@ -51,9 +52,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
chrome.update({ sectionNav: navModel }); chrome.update({ sectionNav: navModel });
}, [chrome, navModel]); }, [chrome, navModel]);
useEffect(() => { useKeyboardShortcuts();
keybindings.setupTimeRangeBindings(false);
}, [keybindings]);
return ( return (
<div className={styles.pageScrollbarWrapper}> <div className={styles.pageScrollbarWrapper}>

View File

@ -0,0 +1,114 @@
import { render } from '@testing-library/react';
import React from 'react';
import { dateTime, EventBusSrv } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime';
import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from 'app/types/events';
import { TestProvider } from '../../../../test/helpers/TestProvider';
import { configureStore } from '../../../store/configureStore';
import { initialExploreState } from '../state/main';
import { makeExplorePaneState } from '../state/utils';
import { useKeyboardShortcuts } from './useKeyboardShortcuts';
const testEventBus = new EventBusSrv();
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
getAppEvents: () => testEventBus,
};
});
const NOW = new Date('2020-10-10T00:00:00.000Z');
function daysFromNow(daysDiff: number) {
return new Date(NOW.getTime() + daysDiff * 86400000);
}
function setup() {
const store = configureStore({
explore: {
...initialExploreState,
panes: {
left: makeExplorePaneState({
range: {
from: dateTime(),
to: dateTime(),
raw: { from: 'now-1d', to: 'now' },
},
}),
right: makeExplorePaneState({
range: {
from: dateTime(),
to: dateTime(),
raw: { from: 'now-2d', to: 'now' },
},
}),
},
},
});
const Wrapper = () => {
useKeyboardShortcuts();
return <div></div>;
};
render(
<TestProvider store={store}>
<Wrapper />
</TestProvider>
);
return store;
}
describe('useKeyboardShortcuts', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(NOW);
});
afterEach(() => {
jest.useRealTimers();
});
it('changes both panes to absolute time range', () => {
const store = setup();
getAppEvents().publish(new AbsoluteTimeEvent({ updateUrl: false }));
const exploreState = store.getState().explore;
const panes = Object.values(exploreState.panes);
expect(panes[0]!.absoluteRange.from).toBe(daysFromNow(-1).getTime());
expect(panes[0]!.absoluteRange.to).toBe(daysFromNow(0).getTime());
expect(panes[1]!.absoluteRange.from).toBe(daysFromNow(-2).getTime());
expect(panes[1]!.absoluteRange.to).toBe(daysFromNow(0).getTime());
});
it('shifts time range in both panes', () => {
const store = setup();
getAppEvents().publish(new ShiftTimeEvent({ direction: ShiftTimeEventDirection.Left }));
const exploreState = store.getState().explore;
const panes = Object.values(exploreState.panes);
expect(panes[0]!.absoluteRange.from).toBe(daysFromNow(-1.5).getTime());
expect(panes[0]!.absoluteRange.to).toBe(daysFromNow(-0.5).getTime());
expect(panes[1]!.absoluteRange.from).toBe(daysFromNow(-3).getTime());
expect(panes[1]!.absoluteRange.to).toBe(daysFromNow(-1).getTime());
});
it('zooms out the time range in both panes', () => {
const store = setup();
getAppEvents().publish(new ZoomOutEvent({ scale: 2 }));
const exploreState = store.getState().explore;
const panes = Object.values(exploreState.panes);
expect(panes[0]!.absoluteRange.from).toBe(daysFromNow(-1.5).getTime());
expect(panes[0]!.absoluteRange.to).toBe(daysFromNow(0.5).getTime());
expect(panes[1]!.absoluteRange.from).toBe(daysFromNow(-3).getTime());
expect(panes[1]!.absoluteRange.to).toBe(daysFromNow(1).getTime());
});
});

View File

@ -0,0 +1,42 @@
import { useEffect } from 'react';
import { Unsubscribable } from 'rxjs';
import { getAppEvents } from '@grafana/runtime';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useDispatch } from 'app/types';
import { AbsoluteTimeEvent, ShiftTimeEvent, ZoomOutEvent } from 'app/types/events';
import { makeAbsoluteTime, shiftTime, zoomOut } from '../state/time';
export function useKeyboardShortcuts() {
const { keybindings } = useGrafana();
const dispatch = useDispatch();
useEffect(() => {
keybindings.setupTimeRangeBindings(false);
const tearDown: Unsubscribable[] = [];
tearDown.push(
getAppEvents().subscribe(AbsoluteTimeEvent, () => {
dispatch(makeAbsoluteTime());
})
);
tearDown.push(
getAppEvents().subscribe(ShiftTimeEvent, (event) => {
dispatch(shiftTime(event.payload.direction));
})
);
tearDown.push(
getAppEvents().subscribe(ZoomOutEvent, (event) => {
dispatch(zoomOut(event.payload.scale));
})
);
return () => {
tearDown.forEach((u) => u.unsubscribe());
};
}, [dispatch, keybindings]);
}

View File

@ -1,5 +1,7 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { EventBusSrv } from '@grafana/data';
import { changeDatasource } from './helper/interactions'; import { changeDatasource } from './helper/interactions';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, tearDown, waitForExplore } from './helper/setup'; import { setupExplore, tearDown, waitForExplore } from './helper/setup';
@ -10,6 +12,13 @@ jest.mock('../../correlations/utils', () => {
}; };
}); });
const testEventBus = new EventBusSrv();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: () => testEventBus,
}));
describe('Explore: handle datasource states', () => { describe('Explore: handle datasource states', () => {
afterEach(() => { afterEach(() => {
tearDown(); tearDown();

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { DataQueryRequest, serializeStateToUrlParam } from '@grafana/data'; import { DataQueryRequest, EventBusSrv, serializeStateToUrlParam } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { LokiQuery } from '../../../plugins/datasource/loki/types';
@ -8,10 +8,13 @@ import { LokiQuery } from '../../../plugins/datasource/loki/types';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, waitForExplore } from './helper/setup'; import { setupExplore, waitForExplore } from './helper/setup';
const testEventBus = new EventBusSrv();
const fetch = jest.fn(); const fetch = jest.fn();
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch }), getBackendSrv: () => ({ fetch }),
getAppEvents: () => testEventBus,
})); }));
jest.mock('app/core/core', () => ({ jest.mock('app/core/core', () => ({

View File

@ -1,10 +1,17 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { serializeStateToUrlParam } from '@grafana/data'; import { EventBusSrv, serializeStateToUrlParam } from '@grafana/data';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, tearDown, waitForExplore } from './helper/setup'; import { setupExplore, tearDown, waitForExplore } from './helper/setup';
const testEventBus = new EventBusSrv();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: () => testEventBus,
}));
jest.mock('../../correlations/utils', () => { jest.mock('../../correlations/utils', () => {
return { return {
getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }), getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }),

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { serializeStateToUrlParam } from '@grafana/data'; import { EventBusSrv, serializeStateToUrlParam } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
@ -13,13 +13,13 @@ import {
assertQueryHistoryComment, assertQueryHistoryComment,
assertQueryHistoryElementsShown, assertQueryHistoryElementsShown,
assertQueryHistoryExists, assertQueryHistoryExists,
assertQueryHistoryIsEmpty,
assertQueryHistoryIsStarred, assertQueryHistoryIsStarred,
assertQueryHistoryTabIsSelected, assertQueryHistoryTabIsSelected,
assertQueryHistoryIsEmpty,
} from './helper/assert'; } from './helper/assert';
import { import {
commentQueryHistory,
closeQueryHistory, closeQueryHistory,
commentQueryHistory,
deleteQueryHistory, deleteQueryHistory,
inputQuery, inputQuery,
loadMoreQueryHistory, loadMoreQueryHistory,
@ -37,12 +37,15 @@ const fetchMock = jest.fn();
const postMock = jest.fn(); const postMock = jest.fn();
const getMock = jest.fn(); const getMock = jest.fn();
const reportInteractionMock = jest.fn(); const reportInteractionMock = jest.fn();
const testEventBus = new EventBusSrv();
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }), getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }),
reportInteraction: (...args: object[]) => { reportInteraction: (...args: object[]) => {
reportInteractionMock(...args); reportInteractionMock(...args);
}, },
getAppEvents: () => testEventBus,
})); }));
jest.mock('app/core/core', () => ({ jest.mock('app/core/core', () => ({

View File

@ -3,23 +3,21 @@ import userEvent from '@testing-library/user-event';
import React, { ComponentProps } from 'react'; import React, { ComponentProps } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { serializeStateToUrlParam } from '@grafana/data'; import { EventBusSrv, serializeStateToUrlParam } from '@grafana/data';
import * as mainState from '../state/main'; import * as mainState from '../state/main';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, waitForExplore } from './helper/setup'; import { setupExplore, waitForExplore } from './helper/setup';
const testEventBus = new EventBusSrv();
jest.mock('app/core/core', () => { jest.mock('app/core/core', () => {
return { return {
contextSrv: { contextSrv: {
hasPermission: () => true, hasPermission: () => true,
hasAccess: () => true, hasAccess: () => true,
}, },
appEvents: {
subscribe: () => {},
publish: () => {},
},
}; };
}); });
@ -36,6 +34,7 @@ const fetch = jest.fn().mockResolvedValue({ correlations: [] });
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'), ...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch }), getBackendSrv: () => ({ fetch }),
getAppEvents: () => testEventBus,
})); }));
jest.mock('rxjs', () => ({ jest.mock('rxjs', () => ({

View File

@ -7,17 +7,6 @@ import { ExploreItemState } from 'app/types';
import { createDefaultInitialState } from './helpers'; import { createDefaultInitialState } from './helpers';
import { changeRangeAction, timeReducer, updateTime } from './time'; import { changeRangeAction, timeReducer, updateTime } from './time';
const MOCK_TIME_RANGE = {};
const mockTimeSrv = {
init: jest.fn(),
timeRange: jest.fn().mockReturnValue(MOCK_TIME_RANGE),
};
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
...jest.requireActual('app/features/dashboard/services/TimeSrv'),
getTimeSrv: () => mockTimeSrv,
}));
const mockTemplateSrv = { const mockTemplateSrv = {
updateTimeRange: jest.fn(), updateTimeRange: jest.fn(),
}; };
@ -29,10 +18,10 @@ jest.mock('@grafana/runtime', () => ({
describe('Explore item reducer', () => { describe('Explore item reducer', () => {
describe('When time is updated', () => { describe('When time is updated', () => {
it('Time service is re-initialized and template service is updated with the new time range', async () => { it('Time service is re-initialized and template service is updated with the new time range', async () => {
const { dispatch } = configureStore(createDefaultInitialState().defaultInitialState as any); const state = createDefaultInitialState().defaultInitialState as any;
const { dispatch } = configureStore(state);
dispatch(updateTime({ exploreId: 'left' })); dispatch(updateTime({ exploreId: 'left' }));
expect(mockTimeSrv.init).toBeCalled(); expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(state.explore.panes.left.range);
expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE);
}); });
}); });

View File

@ -4,12 +4,10 @@ import { AbsoluteTimeRange, dateTimeForTimeZone, LoadingState, RawTimeRange, Tim
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore'; import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/core/utils/explore';
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
import { sortLogsResult } from 'app/features/logs/utils'; import { sortLogsResult } from 'app/features/logs/utils';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors'; import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { ExploreItemState, ThunkResult } from 'app/types'; import { ExploreItemState, ThunkDispatch, ThunkResult } from 'app/types';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { TimeModel } from '../../dashboard/state/TimeModel';
import { syncTimesAction } from './main'; import { syncTimesAction } from './main';
import { runQueries } from './query'; import { runQueries } from './query';
@ -79,21 +77,10 @@ export const updateTime = (config: {
const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth); const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth);
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() }; const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
const timeModel: TimeModel = {
time: range.raw,
refresh: false,
timepicker: {},
getTimezone: () => timeZone,
timeRangeUpdated: (rawTimeRange: RawTimeRange) => {
dispatch(updateTimeRange({ exploreId: exploreId, rawRange: rawTimeRange }));
},
};
// We need to re-initialize TimeSrv because it might have been triggered by the other Explore pane (when split)
getTimeSrv().init(timeModel);
// After re-initializing TimeSrv we need to update the time range in Template service for interpolation // After re-initializing TimeSrv we need to update the time range in Template service for interpolation
// of __from and __to variables // of __from and __to variables
getTemplateSrv().updateTimeRange(getTimeSrv().timeRange()); getTemplateSrv().updateTimeRange(range);
dispatch(changeRangeAction({ exploreId, range, absoluteRange })); dispatch(changeRangeAction({ exploreId, range, absoluteRange }));
}; };
@ -118,24 +105,51 @@ export function syncTimes(exploreId: string): ThunkResult<void> {
}; };
} }
/** function modifyExplorePanesTimeRange(
* Forces the timepicker's time into absolute time. modifier: (
* The conversion is applied to all Explore panes. exploreId: string,
* Useful to produce a bookmarkable URL that points to the same data. exploreItemState: ExploreItemState,
*/ currentTimeRange: TimeRange,
export function makeAbsoluteTime(): ThunkResult<void> { dispatch: ThunkDispatch
) => void
): ThunkResult<void> {
return (dispatch, getState) => { return (dispatch, getState) => {
const timeZone = getTimeZone(getState().user); const timeZone = getTimeZone(getState().user);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user); const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => { Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => {
const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth); const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth);
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() }; modifier(exploreId, exploreItemState!, range, dispatch);
dispatch(updateTime({ exploreId, absoluteRange }));
}); });
}; };
} }
/**
* Forces the timepicker's time into absolute time.
* The conversion is applied to all Explore panes.
* Useful to produce a bookmarkable URL that points to the same data.
*/
export function makeAbsoluteTime(): ThunkResult<void> {
return modifyExplorePanesTimeRange((exploreId, exploreItemState, range, dispatch) => {
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
dispatch(updateTimeRange({ exploreId, absoluteRange }));
});
}
export function shiftTime(direction: number): ThunkResult<void> {
return modifyExplorePanesTimeRange((exploreId, exploreItemState, range, dispatch) => {
const shiftedRange = getShiftedTimeRange(direction, range);
dispatch(updateTimeRange({ exploreId, absoluteRange: shiftedRange }));
});
}
export function zoomOut(scale: number): ThunkResult<void> {
return modifyExplorePanesTimeRange((exploreId, exploreItemState, range, dispatch) => {
const zoomedRange = getZoomedTimeRange(range, scale);
dispatch(updateTimeRange({ exploreId, absoluteRange: zoomedRange }));
});
}
/** /**
* Reducer for an Explore area, to be used by the global Explore reducer. * Reducer for an Explore area, to be used by the global Explore reducer.
*/ */

View File

@ -3,18 +3,18 @@ import { uniq } from 'lodash';
import { import {
AbsoluteTimeRange, AbsoluteTimeRange,
DataSourceApi, DataSourceApi,
dateMath,
DateTime,
EventBusExtended, EventBusExtended,
getDefaultTimeRange, getDefaultTimeRange,
HistoryItem, HistoryItem,
isDateTime,
LoadingState, LoadingState,
LogRowModel, LogRowModel,
PanelData, PanelData,
RawTimeRange, RawTimeRange,
TimeFragment, TimeFragment,
TimeRange, TimeRange,
dateMath,
DateTime,
isDateTime,
toUtc, toUtc,
URLRange, URLRange,
URLRangeValue, URLRangeValue,
@ -42,7 +42,7 @@ export const storeGraphStyle = (graphStyle: string): void => {
/** /**
* Returns a fresh Explore area state * Returns a fresh Explore area state
*/ */
export const makeExplorePaneState = (): ExploreItemState => ({ export const makeExplorePaneState = (overrides?: Partial<ExploreItemState>): ExploreItemState => ({
containerWidth: 0, containerWidth: 0,
datasourceInstance: null, datasourceInstance: null,
history: [], history: [],
@ -73,6 +73,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
supplementaryQueries: loadSupplementaryQueries(), supplementaryQueries: loadSupplementaryQueries(),
panelsState: {}, panelsState: {},
correlations: undefined, correlations: undefined,
...overrides,
}); });
export const createEmptyQueryResponse = (): ExplorePanelData => ({ export const createEmptyQueryResponse = (): ExplorePanelData => ({