Dashboards: Fix timeseries off-by N time shift bug after mouse x-axis zoom in panel (#113821)

fix(panel-time-range): override method to account for time shift
This commit is contained in:
Jesse David Peterson
2025-11-13 12:25:05 -04:00
committed by GitHub
parent ed17fb22dc
commit df44209e53
2 changed files with 113 additions and 0 deletions
@@ -152,6 +152,95 @@ describe('PanelTimeRange', () => {
expect(panelTime.state.from).toBe('now-2h');
expect(panelTime.state.to).toBe('now');
});
describe('onTimeRangeChange', () => {
it('should reverse timeShift when updating time range', () => {
const oneHourShift = '1h';
const panelTime = new PanelTimeRange({ timeShift: oneHourShift });
const sceneTimeRange = new SceneTimeRange({ from: 'now-6h', to: 'now' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const scene = new SceneFlexLayout({
$timeRange: sceneTimeRange,
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
const panelTimeFrom = dateTime('2019-02-11T12:00:00.000Z');
const panelTimeTo = dateTime('2019-02-11T18:00:00.000Z');
panelTime.onTimeRangeChange({
from: panelTimeFrom,
to: panelTimeTo,
raw: { from: panelTimeFrom, to: panelTimeTo },
});
const expectedDashboardTimeFrom = dateTime('2019-02-11T13:00:00.000Z');
const expectedDashboardTimeTo = dateTime('2019-02-11T19:00:00.000Z');
expect(sceneTimeRange.state.value.from.toISOString()).toBe(expectedDashboardTimeFrom.toISOString());
expect(sceneTimeRange.state.value.to.toISOString()).toBe(expectedDashboardTimeTo.toISOString());
});
it('should pass through time range when no timeShift is configured', () => {
const panelTime = new PanelTimeRange({});
const sceneTimeRange = new SceneTimeRange({ from: 'now-6h', to: 'now' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const scene = new SceneFlexLayout({
$timeRange: sceneTimeRange,
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
const userSelectedFrom = dateTime('2019-02-11T12:00:00.000Z');
const userSelectedTo = dateTime('2019-02-11T18:00:00.000Z');
panelTime.onTimeRangeChange({
from: userSelectedFrom,
to: userSelectedTo,
raw: { from: userSelectedFrom, to: userSelectedTo },
});
expect(sceneTimeRange.state.value.from.toISOString()).toBe(userSelectedFrom.toISOString());
expect(sceneTimeRange.state.value.to.toISOString()).toBe(userSelectedTo.toISOString());
});
it('should handle variable expressions in timeShift', () => {
const twoHourShiftValue = '2h';
const customTimeShift = new TestVariable({
name: 'testShift',
value: twoHourShiftValue,
});
const panelTime = new PanelTimeRange({ timeShift: '$testShift' });
const sceneTimeRange = new SceneTimeRange({ from: 'now-6h', to: 'now' });
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
const scene = new SceneFlexLayout({
$variables: new SceneVariableSet({
variables: [customTimeShift],
}),
$timeRange: sceneTimeRange,
children: [new SceneFlexItem({ body: panel })],
});
activateFullSceneTree(scene);
const panelTimeFrom = dateTime('2019-02-11T11:00:00.000Z');
const panelTimeTo = dateTime('2019-02-11T17:00:00.000Z');
panelTime.onTimeRangeChange({
from: panelTimeFrom,
to: panelTimeTo,
raw: { from: panelTimeFrom, to: panelTimeTo },
});
const expectedDashboardTimeFrom = dateTime('2019-02-11T13:00:00.000Z');
const expectedDashboardTimeTo = dateTime('2019-02-11T19:00:00.000Z');
expect(sceneTimeRange.state.value.from.toISOString()).toBe(expectedDashboardTimeFrom.toISOString());
expect(sceneTimeRange.state.value.to.toISOString()).toBe(expectedDashboardTimeTo.toISOString());
});
});
});
function buildAndActivateSceneFor(panelTime: PanelTimeRange) {
@@ -113,6 +113,30 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
);
}
public onTimeRangeChange(timeRange: TimeRange): void {
const { timeShift } = this.state;
if (timeShift) {
const timeShiftInterpolated = sceneGraph.interpolate(this, timeShift);
const reverseShift = '+' + timeShiftInterpolated;
const from = dateMath.parseDateMath(reverseShift, timeRange.from, false);
const to = dateMath.parseDateMath(reverseShift, timeRange.to, true);
if (from && to) {
this.getAncestorTimeRange().onTimeRangeChange({
...timeRange,
from,
to,
raw: { from, to },
});
return;
}
}
this.getAncestorTimeRange().onTimeRangeChange(timeRange);
}
private getTimeOverride(parentTimeRange: TimeRange): TimeOverrideResult {
const { timeFrom, timeShift, compareWith } = this.state;
const infoBlocks = [];