diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index 51146c82b1d..70d9fda91e8 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -40,6 +40,10 @@ const shortcuts = { keys: ['t', '→'], description: 'Move time range forward', }, + { + keys: ['t', 'a'], + description: 'Make time range absolute/permanent', + }, ], }; diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 8356343dd81..a232a133e4f 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -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 })); } diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index db23af30d3c..4892ac510a9 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -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 diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index b243e5f1739..0eda5186131 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -37,6 +37,7 @@ 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 cea1890551c..16d8b0bc0c0 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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; */ export class Explore extends React.PureComponent { scrollElement: HTMLDivElement | undefined; + absoluteTimeUnsubsciber: Unsubscribable | undefined; constructor(props: Props) { super(props); @@ -105,6 +109,14 @@ export class Explore extends React.PureComponent { }; } + 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 { 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, diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 173258ac7d9..9d8e7ab3c4b 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -127,6 +127,29 @@ export function syncTimes(exploreId: ExploreId): 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 { + 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. */ diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 049a36fc319..1acce376945 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -144,6 +144,10 @@ export class ShiftTimeEvent extends BusEventWithPayload { static type = 'shift-time'; } +export class AbsoluteTimeEvent extends BusEventBase { + static type = 'absolute-time'; +} + export class RemovePanelEvent extends BusEventWithPayload { static type = 'remove-panel'; }