diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index d8432179815..4eea9f1e4b0 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { AutoSizerProps } from 'react-virtualized-auto-sizer'; 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 { getPluginLinkExtensions } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; @@ -72,7 +72,6 @@ const dummyProps: Props = { isLive: false, syncedTimes: false, updateTimeRange: jest.fn(), - makeAbsoluteTime: jest.fn(), graphResult: [], absoluteRange: { from: 0, diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 4df94b31c15..bbcefa5cc66 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -4,37 +4,34 @@ import memoizeOne from 'memoize-one'; import React, { createRef } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { Unsubscribable } from 'rxjs'; import { AbsoluteTimeRange, + EventBus, GrafanaTheme2, + hasToggleableQueryFiltersSupport, LoadingState, QueryFixAction, RawTimeRange, - EventBus, SplitOpenOptions, SupplementaryQueryType, - hasToggleableQueryFiltersSupport, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { + AdHocFilterItem, CustomScrollbar, ErrorBoundaryAlert, + PanelContainer, Themeable2, withTheme2, - PanelContainer, - AdHocFilterItem, } from '@grafana/ui'; 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 { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils'; import { StoreState } from 'app/types'; -import { AbsoluteTimeEvent } from 'app/types/events'; import { getTimeZone } from '../profile/state/selectors'; @@ -67,7 +64,7 @@ import { setSupplementaryQueryEnabled, } from './state/query'; import { isSplit } from './state/selectors'; -import { makeAbsoluteTime, updateTimeRange } from './state/time'; +import { updateTimeRange } from './state/time'; const getStyles = (theme: GrafanaTheme2) => { return { @@ -141,10 +138,10 @@ export type Props = ExploreProps & ConnectedProps; */ export class Explore extends React.PureComponent { scrollElement: HTMLDivElement | undefined; - absoluteTimeUnsubsciber: Unsubscribable | undefined; topOfViewRef = createRef(); graphEventBus: EventBus; logsEventBus: EventBus; + memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames); constructor(props: Props) { super(props); @@ -155,14 +152,6 @@ export class Explore extends React.PureComponent { this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false }); } - componentDidMount() { - this.absoluteTimeUnsubsciber = appEvents.subscribe(AbsoluteTimeEvent, this.onMakeAbsoluteTime); - } - - componentWillUnmount() { - this.absoluteTimeUnsubsciber?.unsubscribe(); - } - onChangeTime = (rawRange: RawTimeRange) => { const { updateTimeRange, exploreId } = this.props; updateTimeRange({ exploreId, rawRange }); @@ -228,11 +217,6 @@ export class Explore extends React.PureComponent { this.props.addQueryRow(exploreId, queryKeys.length); }; - onMakeAbsoluteTime = () => { - const { makeAbsoluteTime } = this.props; - makeAbsoluteTime(); - }; - /** * Used by Logs details. */ @@ -450,8 +434,6 @@ export class Explore extends React.PureComponent { ); } - memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames); - renderFlameGraphPanel() { const { queryResponse } = this.props; return ; @@ -660,7 +642,6 @@ const mapDispatchToProps = { scanStopAction, setQueries, updateTimeRange, - makeAbsoluteTime, addQueryRow, splitOpen, setSupplementaryQueryEnabled, diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index d55a300b348..8612491ef9b 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -12,6 +12,7 @@ import { ExploreQueryParams } from 'app/types/explore'; import { ExploreActions } from './ExploreActions'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; +import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater'; import { useStateSync } from './hooks/useStateSync'; 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. // Ultimately the URL is the single source of truth from which state is derived, the page title is not different useExplorePageTitle(props.queryParams); - const { keybindings, chrome } = useGrafana(); + const { chrome } = useGrafana(); const navModel = useNavModel('explore'); const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH); @@ -51,9 +52,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor chrome.update({ sectionNav: navModel }); }, [chrome, navModel]); - useEffect(() => { - keybindings.setupTimeRangeBindings(false); - }, [keybindings]); + useKeyboardShortcuts(); return (
diff --git a/public/app/features/explore/hooks/useKeyboardShortcuts.test.tsx b/public/app/features/explore/hooks/useKeyboardShortcuts.test.tsx new file mode 100644 index 00000000000..da9d1b9969f --- /dev/null +++ b/public/app/features/explore/hooks/useKeyboardShortcuts.test.tsx @@ -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
; + }; + + render( + + + + ); + + 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()); + }); +}); diff --git a/public/app/features/explore/hooks/useKeyboardShortcuts.ts b/public/app/features/explore/hooks/useKeyboardShortcuts.ts new file mode 100644 index 00000000000..48c5bc43208 --- /dev/null +++ b/public/app/features/explore/hooks/useKeyboardShortcuts.ts @@ -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]); +} diff --git a/public/app/features/explore/spec/datasourceState.test.tsx b/public/app/features/explore/spec/datasourceState.test.tsx index 6c727bf40f4..aa4d3fef785 100644 --- a/public/app/features/explore/spec/datasourceState.test.tsx +++ b/public/app/features/explore/spec/datasourceState.test.tsx @@ -1,5 +1,7 @@ import { screen, waitFor } from '@testing-library/react'; +import { EventBusSrv } from '@grafana/data'; + import { changeDatasource } from './helper/interactions'; import { makeLogsQueryResponse } from './helper/query'; 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', () => { afterEach(() => { tearDown(); diff --git a/public/app/features/explore/spec/interpolation.test.tsx b/public/app/features/explore/spec/interpolation.test.tsx index 0d98c2f5cbd..f506d73c039 100644 --- a/public/app/features/explore/spec/interpolation.test.tsx +++ b/public/app/features/explore/spec/interpolation.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { DataQueryRequest, serializeStateToUrlParam } from '@grafana/data'; +import { DataQueryRequest, EventBusSrv, serializeStateToUrlParam } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { LokiQuery } from '../../../plugins/datasource/loki/types'; @@ -8,10 +8,13 @@ import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { makeLogsQueryResponse } from './helper/query'; import { setupExplore, waitForExplore } from './helper/setup'; +const testEventBus = new EventBusSrv(); + const fetch = jest.fn(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch }), + getAppEvents: () => testEventBus, })); jest.mock('app/core/core', () => ({ diff --git a/public/app/features/explore/spec/query.test.tsx b/public/app/features/explore/spec/query.test.tsx index e5ebd638130..21e657cd381 100644 --- a/public/app/features/explore/spec/query.test.tsx +++ b/public/app/features/explore/spec/query.test.tsx @@ -1,10 +1,17 @@ import { screen } from '@testing-library/react'; -import { serializeStateToUrlParam } from '@grafana/data'; +import { EventBusSrv, serializeStateToUrlParam } from '@grafana/data'; import { makeLogsQueryResponse } from './helper/query'; 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', () => { return { getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }), diff --git a/public/app/features/explore/spec/queryHistory.test.tsx b/public/app/features/explore/spec/queryHistory.test.tsx index 1b0eff9d6c3..83dc22ec080 100644 --- a/public/app/features/explore/spec/queryHistory.test.tsx +++ b/public/app/features/explore/spec/queryHistory.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { of } from 'rxjs'; -import { serializeStateToUrlParam } from '@grafana/data'; +import { EventBusSrv, serializeStateToUrlParam } from '@grafana/data'; import { config } from '@grafana/runtime'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; @@ -13,13 +13,13 @@ import { assertQueryHistoryComment, assertQueryHistoryElementsShown, assertQueryHistoryExists, + assertQueryHistoryIsEmpty, assertQueryHistoryIsStarred, assertQueryHistoryTabIsSelected, - assertQueryHistoryIsEmpty, } from './helper/assert'; import { - commentQueryHistory, closeQueryHistory, + commentQueryHistory, deleteQueryHistory, inputQuery, loadMoreQueryHistory, @@ -37,12 +37,15 @@ const fetchMock = jest.fn(); const postMock = jest.fn(); const getMock = jest.fn(); const reportInteractionMock = jest.fn(); +const testEventBus = new EventBusSrv(); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }), reportInteraction: (...args: object[]) => { reportInteractionMock(...args); }, + getAppEvents: () => testEventBus, })); jest.mock('app/core/core', () => ({ diff --git a/public/app/features/explore/spec/split.test.tsx b/public/app/features/explore/spec/split.test.tsx index f84b84c0d27..e2f339ecb22 100644 --- a/public/app/features/explore/spec/split.test.tsx +++ b/public/app/features/explore/spec/split.test.tsx @@ -3,23 +3,21 @@ import userEvent from '@testing-library/user-event'; import React, { ComponentProps } from 'react'; 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 { makeLogsQueryResponse } from './helper/query'; import { setupExplore, waitForExplore } from './helper/setup'; +const testEventBus = new EventBusSrv(); + jest.mock('app/core/core', () => { return { contextSrv: { hasPermission: () => true, hasAccess: () => true, }, - appEvents: { - subscribe: () => {}, - publish: () => {}, - }, }; }); @@ -36,6 +34,7 @@ const fetch = jest.fn().mockResolvedValue({ correlations: [] }); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch }), + getAppEvents: () => testEventBus, })); jest.mock('rxjs', () => ({ diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index 0bd865931b6..55cf86ed393 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -7,17 +7,6 @@ import { ExploreItemState } from 'app/types'; import { createDefaultInitialState } from './helpers'; 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 = { updateTimeRange: jest.fn(), }; @@ -29,10 +18,10 @@ jest.mock('@grafana/runtime', () => ({ describe('Explore item reducer', () => { describe('When time is updated', () => { 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' })); - expect(mockTimeSrv.init).toBeCalled(); - expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE); + expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(state.explore.panes.left.range); }); }); diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 8b720e9b626..85ebd464551 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -4,12 +4,10 @@ import { AbsoluteTimeRange, dateTimeForTimeZone, LoadingState, RawTimeRange, Tim import { getTemplateSrv } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; 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 { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors'; -import { ExploreItemState, ThunkResult } from 'app/types'; - -import { getTimeSrv } from '../../dashboard/services/TimeSrv'; -import { TimeModel } from '../../dashboard/state/TimeModel'; +import { ExploreItemState, ThunkDispatch, ThunkResult } from 'app/types'; import { syncTimesAction } from './main'; import { runQueries } from './query'; @@ -79,21 +77,10 @@ export const updateTime = (config: { const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth); 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 // of __from and __to variables - getTemplateSrv().updateTimeRange(getTimeSrv().timeRange()); + getTemplateSrv().updateTimeRange(range); dispatch(changeRangeAction({ exploreId, range, absoluteRange })); }; @@ -118,24 +105,51 @@ export function syncTimes(exploreId: string): ThunkResult { }; } -/** - * 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 { +function modifyExplorePanesTimeRange( + modifier: ( + exploreId: string, + exploreItemState: ExploreItemState, + currentTimeRange: TimeRange, + dispatch: ThunkDispatch + ) => void +): ThunkResult { return (dispatch, getState) => { const timeZone = getTimeZone(getState().user); const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user); Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => { const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth); - const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() }; - dispatch(updateTime({ exploreId, absoluteRange })); + modifier(exploreId, exploreItemState!, range, dispatch); }); }; } +/** + * 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 { + 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 { + return modifyExplorePanesTimeRange((exploreId, exploreItemState, range, dispatch) => { + const shiftedRange = getShiftedTimeRange(direction, range); + dispatch(updateTimeRange({ exploreId, absoluteRange: shiftedRange })); + }); +} + +export function zoomOut(scale: number): ThunkResult { + 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. */ diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 661b8fd973e..db1d0d28f48 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -3,18 +3,18 @@ import { uniq } from 'lodash'; import { AbsoluteTimeRange, DataSourceApi, + dateMath, + DateTime, EventBusExtended, getDefaultTimeRange, HistoryItem, + isDateTime, LoadingState, LogRowModel, PanelData, RawTimeRange, TimeFragment, TimeRange, - dateMath, - DateTime, - isDateTime, toUtc, URLRange, URLRangeValue, @@ -42,7 +42,7 @@ export const storeGraphStyle = (graphStyle: string): void => { /** * Returns a fresh Explore area state */ -export const makeExplorePaneState = (): ExploreItemState => ({ +export const makeExplorePaneState = (overrides?: Partial): ExploreItemState => ({ containerWidth: 0, datasourceInstance: null, history: [], @@ -73,6 +73,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({ supplementaryQueries: loadSupplementaryQueries(), panelsState: {}, correlations: undefined, + ...overrides, }); export const createEmptyQueryResponse = (): ExplorePanelData => ({