Explore: Add t * keybindings to change time range (#45020)

* Add keybindings to explore, allow override of dashboard model update for explore update

* Remove edits to Dashboard Model, add definition when url params should be updated

* Add tests

* Add and expose util function instead of bringing in unrelated library do not define explicit path to library file

* Use more generic model for TimeSrv

* Remove url utility functions, use javascript function instead

* Break out TimeModel into new type and bring it in

* condense object creation
This commit is contained in:
Kristina 2022-02-17 07:39:02 -06:00 committed by GitHub
parent af1691dbfb
commit 773da0e330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 140 additions and 79 deletions

View File

@ -19,6 +19,28 @@ e2e.scenario({
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
cy.location().then((loc) => {
const params = new URLSearchParams(loc.search);
const leftJSON = JSON.parse(params.get('left'));
expect(leftJSON.range.to).to.equal('now');
expect(leftJSON.range.from).to.equal('now-1h');
cy.get('body').click();
cy.get('body').type('t{leftarrow}');
cy.location().then((locPostKeypress) => {
const params = new URLSearchParams(locPostKeypress.search);
const leftJSON = JSON.parse(params.get('left'));
// be sure the keypress affected the time window
expect(leftJSON.range.to).to.not.equal('now');
expect(leftJSON.range.from).to.not.equal('now-1h');
// be sure the url does not contain dashboard range values
// eslint wants this to be a function, so we use this instead of to.be.false
expect(params.has('to')).to.equal(false);
expect(params.has('from')).to.equal(false);
});
});
const canvases = e2e().get('canvas');
canvases.should('have.length', 1);
},

View File

@ -11,7 +11,7 @@ import { exitKioskMode, toggleKioskMode } from '../navigation/kiosk';
import {
RemovePanelEvent,
ShiftTimeEvent,
ShiftTimeEventPayload,
ShiftTimeEventDirection,
ShowModalReactEvent,
ZoomOutEvent,
AbsoluteTimeEvent,
@ -171,6 +171,24 @@ export class KeybindingSrv {
this.bind(keyArg, withFocusedPanel(fn));
}
setupTimeRangeBindings(updateUrl = true) {
this.bind('t z', () => {
appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl }));
});
this.bind('ctrl+z', () => {
appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl }));
});
this.bind('t left', () => {
appEvents.publish(new ShiftTimeEvent({ direction: ShiftTimeEventDirection.Left, updateUrl }));
});
this.bind('t right', () => {
appEvents.publish(new ShiftTimeEvent({ direction: ShiftTimeEventDirection.Right, updateUrl }));
});
}
setupDashboardBindings(dashboard: DashboardModel) {
this.bind('mod+o', () => {
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
@ -191,21 +209,7 @@ export class KeybindingSrv {
}
});
this.bind('t z', () => {
appEvents.publish(new ZoomOutEvent(2));
});
this.bind('ctrl+z', () => {
appEvents.publish(new ZoomOutEvent(2));
});
this.bind('t left', () => {
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Left));
});
this.bind('t right', () => {
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Right));
});
this.setupTimeRangeBindings();
// edit panel
this.bindWithPanelId('e', (panelId) => {

View File

@ -13,7 +13,7 @@ import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePicker
// Utils & Services
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { appEvents } from 'app/core/core';
import { ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../../types/events';
import { ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../../types/events';
import { Unsubscribable } from 'rxjs';
export interface Props {
@ -38,16 +38,16 @@ export class DashNavTimeControls extends Component<Props> {
};
onRefresh = () => {
getTimeSrv().refreshDashboard();
getTimeSrv().refreshTimeModel();
return Promise.resolve();
};
onMoveBack = () => {
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Left));
appEvents.publish(new ShiftTimeEvent({ direction: ShiftTimeEventDirection.Left }));
};
onMoveForward = () => {
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Right));
appEvents.publish(new ShiftTimeEvent({ direction: ShiftTimeEventDirection.Right }));
};
onChangeTimePicker = (timeRange: TimeRange) => {
@ -77,7 +77,7 @@ export class DashNavTimeControls extends Component<Props> {
};
onZoom = () => {
appEvents.publish(new ZoomOutEvent(2));
appEvents.publish(new ZoomOutEvent({ scale: 2 }));
};
render() {

View File

@ -244,6 +244,11 @@ describe('timeSrv', () => {
expect(locationUpdates[1].search).toEqual('?kiosk&from=now-1h&to=now-10s');
});
it('should not change the URL if the updateUrl param is false', () => {
timeSrv.setTime({ from: '1644340584281', to: '1644340584281' }, false);
expect(locationUpdates.length).toBe(0);
});
});
describe('pauseAutoRefresh', () => {

View File

@ -9,14 +9,14 @@ import {
TimeRange,
toUtc,
} from '@grafana/data';
import { DashboardModel } from '../state/DashboardModel';
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
import { config } from 'app/core/config';
import { getRefreshFromUrl } from '../utils/getRefreshFromUrl';
import { locationService } from '@grafana/runtime';
import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../types/events';
import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../types/events';
import { contextSrv, ContextSrv } from 'app/core/services/context_srv';
import appEvents from 'app/core/app_events';
import { TimeModel } from '../state/TimeModel';
export class TimeSrv {
time: any;
@ -24,21 +24,21 @@ export class TimeSrv {
refresh: any;
previousAutoRefresh: any;
oldRefresh: string | null | undefined;
dashboard?: DashboardModel;
timeModel?: TimeModel;
timeAtLoad: any;
private autoRefreshBlocked?: boolean;
constructor(private contextSrv: ContextSrv) {
// default time
this.time = getDefaultTimeRange().raw;
this.refreshDashboard = this.refreshDashboard.bind(this);
this.refreshTimeModel = this.refreshTimeModel.bind(this);
appEvents.subscribe(ZoomOutEvent, (e) => {
this.zoomOut(e.payload);
this.zoomOut(e.payload.scale, e.payload.updateUrl);
});
appEvents.subscribe(ShiftTimeEvent, (e) => {
this.shiftTime(e.payload);
this.shiftTime(e.payload.direction, e.payload.updateUrl);
});
appEvents.subscribe(AbsoluteTimeEvent, () => {
@ -48,15 +48,15 @@ export class TimeSrv {
document.addEventListener('visibilitychange', () => {
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
this.autoRefreshBlocked = false;
this.refreshDashboard();
this.refreshTimeModel();
}
});
}
init(dashboard: DashboardModel) {
this.dashboard = dashboard;
this.time = dashboard.time;
this.refresh = dashboard.refresh;
init(timeModel: TimeModel) {
this.timeModel = timeModel;
this.time = timeModel.time;
this.refresh = timeModel.refresh;
this.initTimeFromUrl();
this.parseTime();
@ -66,8 +66,8 @@ export class TimeSrv {
const range = rangeUtil.convertRawToRange(
this.time,
this.dashboard?.getTimezone(),
this.dashboard?.fiscalYearStartMonth
this.timeModel?.getTimezone(),
this.timeModel?.fiscalYearStartMonth
);
if (range.to.isBefore(range.from)) {
@ -159,11 +159,11 @@ export class TimeSrv {
this.time.to = this.parseUrlParam(params.get('to')!) || this.time.to;
}
// if absolute ignore refresh option saved to dashboard
// if absolute ignore refresh option saved to timeModel
if (params.get('to') && params.get('to')!.indexOf('now') === -1) {
this.refresh = false;
if (this.dashboard) {
this.dashboard.refresh = false;
if (this.timeModel) {
this.timeModel.refresh = false;
}
}
@ -176,8 +176,8 @@ export class TimeSrv {
this.refresh = getRefreshFromUrl({
params: paramsJSON,
currentRefresh: this.refresh,
refreshIntervals: Array.isArray(this.dashboard?.timepicker?.refresh_intervals)
? this.dashboard?.timepicker?.refresh_intervals
refreshIntervals: Array.isArray(this.timeModel?.timepicker?.refresh_intervals)
? this.timeModel?.timepicker?.refresh_intervals
: undefined,
isAllowedIntervalFn: this.contextSrv.isAllowedInterval,
minRefreshInterval: config.minRefreshInterval,
@ -213,8 +213,8 @@ export class TimeSrv {
}
setAutoRefresh(interval: any) {
if (this.dashboard) {
this.dashboard.refresh = interval;
if (this.timeModel) {
this.timeModel.refresh = interval;
}
this.stopAutoRefresh();
@ -235,7 +235,7 @@ export class TimeSrv {
this.refreshTimer = setTimeout(() => {
this.startNextRefreshTimer(intervalMs);
this.refreshDashboard();
this.refreshTimeModel();
}, intervalMs);
const refresh = this.contextSrv.getValidInterval(interval);
@ -245,15 +245,15 @@ export class TimeSrv {
}
}
refreshDashboard() {
this.dashboard?.timeRangeUpdated(this.timeRange());
refreshTimeModel() {
this.timeModel?.timeRangeUpdated(this.timeRange());
}
private startNextRefreshTimer(afterMs: number) {
this.refreshTimer = setTimeout(() => {
this.startNextRefreshTimer(afterMs);
if (this.contextSrv.isGrafanaVisible()) {
this.refreshDashboard();
this.refreshTimeModel();
} else {
this.autoRefreshBlocked = true;
}
@ -264,10 +264,10 @@ export class TimeSrv {
clearTimeout(this.refreshTimer);
}
// store dashboard refresh value and pause auto-refresh in some places
// store timeModel refresh value and pause auto-refresh in some places
// i.e panel edit
pauseAutoRefresh() {
this.previousAutoRefresh = this.dashboard?.refresh;
this.previousAutoRefresh = this.timeModel?.refresh;
this.setAutoRefresh('');
}
@ -276,20 +276,19 @@ export class TimeSrv {
this.setAutoRefresh(this.previousAutoRefresh);
}
setTime(time: RawTimeRange, fromRouteUpdate?: boolean) {
setTime(time: RawTimeRange, updateUrl = true) {
extend(this.time, time);
// disable refresh if zoom in or zoom out
if (isDateTime(time.to)) {
this.oldRefresh = this.dashboard?.refresh || this.oldRefresh;
this.oldRefresh = this.timeModel?.refresh || this.oldRefresh;
this.setAutoRefresh(false);
} else if (this.oldRefresh && this.oldRefresh !== this.dashboard?.refresh) {
} else if (this.oldRefresh && this.oldRefresh !== this.timeModel?.refresh) {
this.setAutoRefresh(this.oldRefresh);
this.oldRefresh = null;
}
// update url
if (fromRouteUpdate !== true) {
if (updateUrl === true) {
const urlRange = this.timeRangeForUrl();
const urlParams = locationService.getSearchObject();
@ -303,7 +302,7 @@ export class TimeSrv {
locationService.partial(urlParams);
}
this.refreshDashboard();
this.refreshTimeModel();
}
timeRangeForUrl = () => {
@ -326,30 +325,33 @@ export class TimeSrv {
to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to,
};
const timezone = this.dashboard ? this.dashboard.getTimezone() : undefined;
const timezone = this.timeModel ? this.timeModel.getTimezone() : undefined;
return {
from: dateMath.parse(raw.from, false, timezone, this.dashboard?.fiscalYearStartMonth)!,
to: dateMath.parse(raw.to, true, timezone, this.dashboard?.fiscalYearStartMonth)!,
from: dateMath.parse(raw.from, false, timezone, this.timeModel?.fiscalYearStartMonth)!,
to: dateMath.parse(raw.to, true, timezone, this.timeModel?.fiscalYearStartMonth)!,
raw: raw,
};
}
zoomOut(factor: number) {
zoomOut(factor: number, updateUrl = true) {
const range = this.timeRange();
const { from, to } = getZoomedTimeRange(range, factor);
this.setTime({ from: toUtc(from), to: toUtc(to) });
this.setTime({ from: toUtc(from), to: toUtc(to) }, updateUrl);
}
shiftTime(direction: ShiftTimeEventPayload) {
shiftTime(direction: ShiftTimeEventDirection, updateUrl = true) {
const range = this.timeRange();
const { from, to } = getShiftedTimeRange(direction, range);
this.setTime({
from: toUtc(from),
to: toUtc(to),
});
this.setTime(
{
from: toUtc(from),
to: toUtc(to),
},
updateUrl
);
}
makeAbsoluteTime() {
@ -359,7 +361,7 @@ export class TimeSrv {
}
const { from, to } = this.timeRange();
this.setTime({ from, to });
this.setTime({ from, to }, true);
}
// isRefreshOutsideThreshold function calculates the difference between last refresh and now

View File

@ -20,6 +20,7 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, REPEAT_DIR_VERT
import { contextSrv } from 'app/core/services/context_srv';
// Types
import { GridPos, PanelModel } from './PanelModel';
import { TimeModel } from './TimeModel';
import { DashboardMigrator } from './DashboardMigrator';
import {
AnnotationQuery,
@ -78,7 +79,7 @@ export interface DashboardLink {
includeVars: boolean;
}
export class DashboardModel {
export class DashboardModel implements TimeModel {
id: any;
uid: string;
title: string;

View File

@ -0,0 +1,10 @@
import { TimeRange, TimeZone } from '@grafana/data';
export interface TimeModel {
time: any;
fiscalYearStartMonth?: number;
refresh: any;
timepicker: any;
getTimezone(): TimeZone;
timeRangeUpdated(timeRange: TimeRange): void;
}

View File

@ -131,12 +131,12 @@ export const updateTimeZoneDashboard =
(timeZone: TimeZone): ThunkResult<void> =>
(dispatch) => {
dispatch(updateTimeZoneForSession(timeZone));
getTimeSrv().refreshDashboard();
getTimeSrv().refreshTimeModel();
};
export const updateWeekStartDashboard =
(weekStart: string): ThunkResult<void> =>
(dispatch) => {
dispatch(updateWeekStartForSession(weekStart));
getTimeSrv().refreshDashboard();
getTimeSrv().refreshTimeModel();
};

View File

@ -38,6 +38,7 @@ import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state
import { getDataSourceSrv } from '@grafana/runtime';
import { getRichHistory } from '../../../core/utils/richHistory';
import { richHistoryUpdatedAction, stateSave } from './main';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
//
// Actions and Payloads
@ -172,6 +173,8 @@ export function initializeExplore(
}
dispatch(updateTime({ exploreId }));
keybindingSrv.setupTimeRangeBindings(false);
if (instance) {
// We do not want to add the url to browser history on init because when the pane is initialised it's because
// we already have something in the url. Adding basically the same state as additional history item prevents

View File

@ -14,7 +14,7 @@ import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { TimeModel } from '../../dashboard/state/TimeModel';
import { runQueries } from './query';
import { syncTimesAction, stateSave } from './main';
@ -95,14 +95,17 @@ export const updateTime = (config: {
const range = getTimeRange(timeZone, rawRange, fiscalYearStartMonth);
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 }));
},
};
getTimeSrv().init(
new DashboardModel({
time: range.raw,
refresh: false,
timeZone,
})
);
getTimeSrv().init(timeModel);
dispatch(changeRangeAction({ exploreId, range, absoluteRange }));
};

View File

@ -187,7 +187,7 @@ export class GraphCtrl extends MetricsPanelCtrl {
}
zoomOut(evt: any) {
appEvents.publish(new ZoomOutEvent(2));
appEvents.publish(new ZoomOutEvent({ scale: 2 }));
}
onDataSnapshotLoad(snapshotData: any) {

View File

@ -161,7 +161,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
}
zoomOut(evt: any) {
appEvents.publish(new ZoomOutEvent(2));
appEvents.publish(new ZoomOutEvent({ scale: 2 }));
}
onRender() {

View File

@ -132,14 +132,25 @@ export class RenderEvent extends BusEventBase {
static type = 'render';
}
export class ZoomOutEvent extends BusEventWithPayload<number> {
interface ZoomOutEventPayload {
scale: number;
updateUrl?: boolean;
}
export class ZoomOutEvent extends BusEventWithPayload<ZoomOutEventPayload> {
static type = 'zoom-out';
}
export enum ShiftTimeEventPayload {
export enum ShiftTimeEventDirection {
Left = -1,
Right = 1,
}
interface ShiftTimeEventPayload {
direction: ShiftTimeEventDirection;
updateUrl?: boolean;
}
export class ShiftTimeEvent extends BusEventWithPayload<ShiftTimeEventPayload> {
static type = 'shift-time';
}