mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
79d10c6903
commit
a08e0581de
@ -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',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 }));
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user