mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8b7566c299
commit
f11cc0e60e
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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}>
|
||||||
|
114
public/app/features/explore/hooks/useKeyboardShortcuts.test.tsx
Normal file
114
public/app/features/explore/hooks/useKeyboardShortcuts.test.tsx
Normal 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());
|
||||||
|
});
|
||||||
|
});
|
42
public/app/features/explore/hooks/useKeyboardShortcuts.ts
Normal file
42
public/app/features/explore/hooks/useKeyboardShortcuts.ts
Normal 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]);
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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', () => ({
|
||||||
|
@ -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: [] }),
|
||||||
|
@ -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', () => ({
|
||||||
|
@ -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', () => ({
|
||||||
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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 => ({
|
||||||
|
Loading…
Reference in New Issue
Block a user