Hotkeys: Make time range permanent (#43802)

Typing `t a` in Explore or Dashboards will turn a relative time like "Last 1 hour"
into an absolute range to make the URL permanent, so that when sharing it others
will see the same data.

- registered `t a` in key service
- new `AbsoluteTimeEvent` dispatch via global event bus
- dashboard times handled in TimeSrv
- Explore times handled in Explore.tsx and Explore's time reducer

I could not find an easy way to combine time handling for Exlore and Dashboard in one place.
This commit is contained in:
David 2022-01-07 16:51:29 +01:00 committed by GitHub
parent 79d10c6903
commit a08e0581de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 2 deletions

View File

@ -40,6 +40,10 @@ const shortcuts = {
keys: ['t', '→'], keys: ['t', '→'],
description: 'Move time range forward', description: 'Move time range forward',
}, },
{
keys: ['t', 'a'],
description: 'Make time range absolute/permanent',
},
], ],
}; };

View File

@ -14,6 +14,7 @@ import {
ShiftTimeEventPayload, ShiftTimeEventPayload,
ShowModalReactEvent, ShowModalReactEvent,
ZoomOutEvent, ZoomOutEvent,
AbsoluteTimeEvent,
} from '../../types/events'; } from '../../types/events';
import { contextSrv } from '../core'; import { contextSrv } from '../core';
import { getDatasourceSrv } from '../../features/plugins/datasource_srv'; import { getDatasourceSrv } from '../../features/plugins/datasource_srv';
@ -34,6 +35,7 @@ export class KeybindingSrv {
this.bind('g a', this.openAlerting); this.bind('g a', this.openAlerting);
this.bind('g p', this.goToProfile); this.bind('g p', this.goToProfile);
this.bind('s o', this.openSearch); this.bind('s o', this.openSearch);
this.bind('t a', this.makeAbsoluteTime);
this.bind('f', this.openSearch); this.bind('f', this.openSearch);
this.bind('esc', this.exit); this.bind('esc', this.exit);
this.bindGlobal('esc', this.globalEsc); this.bindGlobal('esc', this.globalEsc);
@ -89,6 +91,10 @@ export class KeybindingSrv {
locationService.push('/profile'); locationService.push('/profile');
} }
private makeAbsoluteTime() {
appEvents.publish(new AbsoluteTimeEvent());
}
private showHelpModal() { private showHelpModal() {
appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
} }

View File

@ -14,7 +14,7 @@ import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePick
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { getRefreshFromUrl } from '../utils/getRefreshFromUrl'; import { getRefreshFromUrl } from '../utils/getRefreshFromUrl';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../types/events'; import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../types/events';
import { contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { contextSrv, ContextSrv } from 'app/core/services/context_srv';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
@ -41,6 +41,10 @@ export class TimeSrv {
this.shiftTime(e.payload); this.shiftTime(e.payload);
}); });
appEvents.subscribe(AbsoluteTimeEvent, () => {
this.makeAbsoluteTime();
});
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (this.autoRefreshBlocked && document.visibilityState === 'visible') { if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
this.autoRefreshBlocked = false; this.autoRefreshBlocked = false;
@ -348,6 +352,16 @@ export class TimeSrv {
}); });
} }
makeAbsoluteTime() {
const params = locationService.getSearch();
if (params.get('left')) {
return; // explore handles this;
}
const { from, to } = this.timeRange();
this.setTime({ from, to });
}
// isRefreshOutsideThreshold function calculates the difference between last refresh and now // isRefreshOutsideThreshold function calculates the difference between last refresh and now
// if the difference is outside 5% of the current set time range then the function will return true // if the difference is outside 5% of the current set time range then the function will return true
// if the difference is within 5% of the current set time range then the function will return false // if the difference is within 5% of the current set time range then the function will return false

View File

@ -37,6 +37,7 @@ 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

@ -15,7 +15,7 @@ import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector'; import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main'; import { splitOpen } from './state/main';
import { changeSize, changeGraphStyle } from './state/explorePane'; import { changeSize, changeGraphStyle } from './state/explorePane';
import { updateTimeRange } from './state/time'; import { makeAbsoluteTime, updateTimeRange } from './state/time';
import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query'; import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
@ -31,6 +31,9 @@ import { ExploreGraph } from './ExploreGraph';
import { LogsVolumePanel } from './LogsVolumePanel'; import { LogsVolumePanel } from './LogsVolumePanel';
import { ExploreGraphLabel } from './ExploreGraphLabel'; import { ExploreGraphLabel } from './ExploreGraphLabel';
import { ExploreGraphStyle } from 'app/core/utils/explore'; import { ExploreGraphStyle } from 'app/core/utils/explore';
import appEvents from 'app/core/app_events';
import { AbsoluteTimeEvent } from 'app/types/events';
import { Unsubscribable } from 'rxjs';
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
@ -97,6 +100,7 @@ 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;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@ -105,6 +109,14 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
}; };
} }
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 });
@ -139,6 +151,11 @@ 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();
};
onModifyQueries = (action: any, index?: number) => { onModifyQueries = (action: any, index?: number) => {
const { datasourceInstance } = this.props; const { datasourceInstance } = this.props;
if (datasourceInstance?.modifyQuery) { if (datasourceInstance?.modifyQuery) {
@ -447,6 +464,7 @@ const mapDispatchToProps = {
scanStopAction, scanStopAction,
setQueries, setQueries,
updateTimeRange, updateTimeRange,
makeAbsoluteTime,
loadLogsVolumeData, loadLogsVolumeData,
addQueryRow, addQueryRow,
splitOpen, splitOpen,

View File

@ -127,6 +127,29 @@ export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
}; };
} }
/**
* 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 (dispatch, getState) => {
const timeZone = getTimeZone(getState().user);
const fiscalYearStartMonth = getFiscalYearStartMonth(getState().user);
const leftState = getState().explore.left;
const leftRange = getTimeRange(timeZone, leftState.range.raw, fiscalYearStartMonth);
const leftAbsoluteRange: AbsoluteTimeRange = { from: leftRange.from.valueOf(), to: leftRange.to.valueOf() };
dispatch(updateTime({ exploreId: ExploreId.left, absoluteRange: leftAbsoluteRange }));
const rightState = getState().explore.right!;
if (rightState) {
const rightRange = getTimeRange(timeZone, rightState.range.raw, fiscalYearStartMonth);
const rightAbsoluteRange: AbsoluteTimeRange = { from: rightRange.from.valueOf(), to: rightRange.to.valueOf() };
dispatch(updateTime({ exploreId: ExploreId.right, absoluteRange: rightAbsoluteRange }));
}
dispatch(stateSave());
};
}
/** /**
* 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

@ -144,6 +144,10 @@ export class ShiftTimeEvent extends BusEventWithPayload<ShiftTimeEventPayload> {
static type = 'shift-time'; static type = 'shift-time';
} }
export class AbsoluteTimeEvent extends BusEventBase {
static type = 'absolute-time';
}
export class RemovePanelEvent extends BusEventWithPayload<number> { export class RemovePanelEvent extends BusEventWithPayload<number> {
static type = 'remove-panel'; static type = 'remove-panel';
} }