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', '→'],
description: 'Move time range forward',
},
{
keys: ['t', 'a'],
description: 'Make time range absolute/permanent',
},
],
};

View File

@ -14,6 +14,7 @@ import {
ShiftTimeEventPayload,
ShowModalReactEvent,
ZoomOutEvent,
AbsoluteTimeEvent,
} from '../../types/events';
import { contextSrv } from '../core';
import { getDatasourceSrv } from '../../features/plugins/datasource_srv';
@ -34,6 +35,7 @@ export class KeybindingSrv {
this.bind('g a', this.openAlerting);
this.bind('g p', this.goToProfile);
this.bind('s o', this.openSearch);
this.bind('t a', this.makeAbsoluteTime);
this.bind('f', this.openSearch);
this.bind('esc', this.exit);
this.bindGlobal('esc', this.globalEsc);
@ -89,6 +91,10 @@ export class KeybindingSrv {
locationService.push('/profile');
}
private makeAbsoluteTime() {
appEvents.publish(new AbsoluteTimeEvent());
}
private showHelpModal() {
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 { getRefreshFromUrl } from '../utils/getRefreshFromUrl';
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 appEvents from 'app/core/app_events';
@ -41,6 +41,10 @@ export class TimeSrv {
this.shiftTime(e.payload);
});
appEvents.subscribe(AbsoluteTimeEvent, () => {
this.makeAbsoluteTime();
});
document.addEventListener('visibilitychange', () => {
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
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
// 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

View File

@ -37,6 +37,7 @@ const dummyProps: Props = {
isLive: false,
syncedTimes: false,
updateTimeRange: jest.fn(),
makeAbsoluteTime: jest.fn(),
graphResult: [],
absoluteRange: {
from: 0,

View File

@ -15,7 +15,7 @@ import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main';
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 { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
@ -31,6 +31,9 @@ import { ExploreGraph } from './ExploreGraph';
import { LogsVolumePanel } from './LogsVolumePanel';
import { ExploreGraphLabel } from './ExploreGraphLabel';
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) => {
return {
@ -97,6 +100,7 @@ export type Props = ExploreProps & ConnectedProps<typeof connector>;
*/
export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined;
constructor(props: 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) => {
const { updateTimeRange, exploreId } = this.props;
updateTimeRange({ exploreId, rawRange });
@ -139,6 +151,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
this.props.addQueryRow(exploreId, queryKeys.length);
};
onMakeAbsoluteTime = () => {
const { makeAbsoluteTime } = this.props;
makeAbsoluteTime();
};
onModifyQueries = (action: any, index?: number) => {
const { datasourceInstance } = this.props;
if (datasourceInstance?.modifyQuery) {
@ -447,6 +464,7 @@ const mapDispatchToProps = {
scanStopAction,
setQueries,
updateTimeRange,
makeAbsoluteTime,
loadLogsVolumeData,
addQueryRow,
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.
*/

View File

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