Skip to content

Commit 749cc73

Browse files
authored
Timeline: Add timeRangePan (grafana#113890)
* Timeline: Add timeRangePan * Add tests for state timeline * Add tests for status history * Fix state timeline test and use same dashboard * Consolidate tests * Optimize time range pan tests
1 parent 7888f19 commit 749cc73

File tree

5 files changed

+229
-1
lines changed

5 files changed

+229
-1
lines changed

e2e-playwright/panels-suite/state-timeline.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,112 @@ test.describe('Panels test: StateTimeine', { tag: ['@panels', '@state-timeline']
8787
await expect(tooltip, 'tooltip is not shown when disabled').toBeHidden();
8888
});
8989
});
90+
91+
test.use({
92+
featureToggles: {
93+
timeRangePan: true,
94+
},
95+
});
96+
97+
test.describe('Panels test: State Timeline X-axis panning', { tag: ['@panels', '@state-timeline'] }, () => {
98+
test('x-axis panning functionality', async ({ gotoDashboardPage, page, selectors }) => {
99+
let centerX: number;
100+
let centerY: number;
101+
let initialFromTime: number;
102+
let initialToTime: number;
103+
104+
const dashboardPage = await test.step('Load dashboard and verify cursor changes to grab', async () => {
105+
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
106+
107+
const stateTimelinePanel = page.locator('.uplot').first();
108+
await expect(stateTimelinePanel, 'panel rendered').toBeVisible();
109+
110+
const xAxis = stateTimelinePanel.locator('.u-axis').first();
111+
await expect(xAxis, 'x-axis rendered').toBeVisible();
112+
113+
await xAxis.hover();
114+
115+
const cursorStyle = await xAxis.evaluate((el: HTMLElement) => window.getComputedStyle(el).cursor);
116+
expect(cursorStyle, 'cursor is grab').toBe('grab');
117+
118+
return dashboardPage;
119+
});
120+
121+
await test.step('Capture initial time range', async () => {
122+
const stateTimelinePanel = page.locator('.uplot').first();
123+
const xAxis = stateTimelinePanel.locator('.u-axis').first();
124+
125+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
126+
await timePickerButton.click();
127+
128+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
129+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
130+
131+
const initialFrom = await fromField.inputValue();
132+
const initialTo = await toField.inputValue();
133+
initialFromTime = new Date(initialFrom).getTime();
134+
initialToTime = new Date(initialTo).getTime();
135+
136+
await page.keyboard.press('Escape');
137+
138+
const axisBox = await xAxis.boundingBox();
139+
if (!axisBox) {
140+
throw new Error('X-axis bounding box not found');
141+
}
142+
143+
centerX = axisBox.x + axisBox.width / 2;
144+
centerY = axisBox.y + axisBox.height / 2;
145+
});
146+
147+
await test.step('Drag right pans backward in time', async () => {
148+
await page.mouse.move(centerX, centerY);
149+
await page.mouse.down();
150+
await page.mouse.move(centerX + 100, centerY);
151+
await page.mouse.up();
152+
153+
await page.waitForTimeout(1000);
154+
155+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
156+
await timePickerButton.click();
157+
158+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
159+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
160+
161+
const afterRightFrom = await fromField.inputValue();
162+
const afterRightTo = await toField.inputValue();
163+
const afterRightFromTime = new Date(afterRightFrom).getTime();
164+
const afterRightToTime = new Date(afterRightTo).getTime();
165+
166+
expect(afterRightFromTime, 'panned backward').toBeLessThan(initialFromTime);
167+
expect(afterRightToTime, 'panned backward').toBeLessThan(initialToTime);
168+
169+
await page.keyboard.press('Escape');
170+
171+
initialFromTime = afterRightFromTime;
172+
initialToTime = afterRightToTime;
173+
});
174+
175+
await test.step('Drag left pans forward in time', async () => {
176+
await page.mouse.move(centerX, centerY);
177+
await page.mouse.down();
178+
await page.mouse.move(centerX - 100, centerY);
179+
await page.mouse.up();
180+
181+
await page.waitForTimeout(1000);
182+
183+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
184+
await timePickerButton.click();
185+
186+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
187+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
188+
189+
const afterLeftFrom = await fromField.inputValue();
190+
const afterLeftTo = await toField.inputValue();
191+
const afterLeftFromTime = new Date(afterLeftFrom).getTime();
192+
const afterLeftToTime = new Date(afterLeftTo).getTime();
193+
194+
expect(afterLeftFromTime, 'panned forward').toBeGreaterThan(initialFromTime);
195+
expect(afterLeftToTime, 'panned forward').toBeGreaterThan(initialToTime);
196+
});
197+
});
198+
});

e2e-playwright/panels-suite/status-history.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,112 @@ test.describe('Panels test: StatusHistory', { tag: ['@panels', '@status-history'
8585
await expect(tooltip, 'tooltip is not shown when disabled').toBeHidden();
8686
});
8787
});
88+
89+
test.use({
90+
featureToggles: {
91+
timeRangePan: true,
92+
},
93+
});
94+
95+
test.describe('Panels test: Status History X-axis panning', { tag: ['@panels', '@status-history'] }, () => {
96+
test('x-axis panning functionality', async ({ gotoDashboardPage, page, selectors }) => {
97+
let centerX: number;
98+
let centerY: number;
99+
let initialFromTime: number;
100+
let initialToTime: number;
101+
102+
const dashboardPage = await test.step('Load dashboard and verify cursor changes to grab', async () => {
103+
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
104+
105+
const statusHistoryPanel = page.locator('.uplot').first();
106+
await expect(statusHistoryPanel, 'panel rendered').toBeVisible();
107+
108+
const xAxis = statusHistoryPanel.locator('.u-axis').first();
109+
await expect(xAxis, 'x-axis rendered').toBeVisible();
110+
111+
await xAxis.hover();
112+
113+
const cursorStyle = await xAxis.evaluate((el: HTMLElement) => window.getComputedStyle(el).cursor);
114+
expect(cursorStyle, 'cursor is grab').toBe('grab');
115+
116+
return dashboardPage;
117+
});
118+
119+
await test.step('Capture initial time range', async () => {
120+
const statusHistoryPanel = page.locator('.uplot').first();
121+
const xAxis = statusHistoryPanel.locator('.u-axis').first();
122+
123+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
124+
await timePickerButton.click();
125+
126+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
127+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
128+
129+
const initialFrom = await fromField.inputValue();
130+
const initialTo = await toField.inputValue();
131+
initialFromTime = new Date(initialFrom).getTime();
132+
initialToTime = new Date(initialTo).getTime();
133+
134+
await page.keyboard.press('Escape');
135+
136+
const axisBox = await xAxis.boundingBox();
137+
if (!axisBox) {
138+
throw new Error('X-axis bounding box not found');
139+
}
140+
141+
centerX = axisBox.x + axisBox.width / 2;
142+
centerY = axisBox.y + axisBox.height / 2;
143+
});
144+
145+
await test.step('Drag right pans backward in time', async () => {
146+
await page.mouse.move(centerX, centerY);
147+
await page.mouse.down();
148+
await page.mouse.move(centerX + 100, centerY);
149+
await page.mouse.up();
150+
151+
await page.waitForTimeout(1000);
152+
153+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
154+
await timePickerButton.click();
155+
156+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
157+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
158+
159+
const afterRightFrom = await fromField.inputValue();
160+
const afterRightTo = await toField.inputValue();
161+
const afterRightFromTime = new Date(afterRightFrom).getTime();
162+
const afterRightToTime = new Date(afterRightTo).getTime();
163+
164+
expect(afterRightFromTime, 'panned backward').toBeLessThan(initialFromTime);
165+
expect(afterRightToTime, 'panned backward').toBeLessThan(initialToTime);
166+
167+
await page.keyboard.press('Escape');
168+
169+
initialFromTime = afterRightFromTime;
170+
initialToTime = afterRightToTime;
171+
});
172+
173+
await test.step('Drag left pans forward in time', async () => {
174+
await page.mouse.move(centerX, centerY);
175+
await page.mouse.down();
176+
await page.mouse.move(centerX - 100, centerY);
177+
await page.mouse.up();
178+
179+
await page.waitForTimeout(1000);
180+
181+
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
182+
await timePickerButton.click();
183+
184+
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
185+
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
186+
187+
const afterLeftFrom = await fromField.inputValue();
188+
const afterLeftTo = await toField.inputValue();
189+
const afterLeftFromTime = new Date(afterLeftFrom).getTime();
190+
const afterLeftToTime = new Date(afterLeftTo).getTime();
191+
192+
expect(afterLeftFromTime, 'panned forward').toBeGreaterThan(initialFromTime);
193+
expect(afterLeftToTime, 'panned forward').toBeGreaterThan(initialToTime);
194+
});
195+
});
196+
});

public/app/core/components/TimelineChart/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
156156
isTime: true,
157157
orientation: ScaleOrientation.Horizontal,
158158
direction: ScaleDirection.Right,
159-
range: coreConfig.xRange,
159+
range: (u) => {
160+
const state = builder.getState();
161+
if (state.isPanning) {
162+
return [state.min, state.max];
163+
}
164+
return coreConfig.xRange(u);
165+
},
160166
});
161167

162168
builder.addScale({

public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
EventBusPlugin,
88
TooltipDisplayMode,
99
TooltipPlugin2,
10+
XAxisInteractionAreaPlugin,
1011
usePanelContext,
1112
useTheme2,
1213
} from '@grafana/ui';
@@ -102,6 +103,7 @@ export const StateTimelinePanel = ({
102103
{cursorSync !== DashboardCursorSync.Off && (
103104
<EventBusPlugin config={builder} eventBus={eventBus} frame={alignedFrame} />
104105
)}
106+
<XAxisInteractionAreaPlugin config={builder} queryZoom={onChangeTimeRange} />
105107
{options.tooltip.mode !== TooltipDisplayMode.None && (
106108
<TooltipPlugin2
107109
config={builder}

public/app/plugins/panel/status-history/StatusHistoryPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
TooltipPlugin2,
1111
usePanelContext,
1212
useTheme2,
13+
XAxisInteractionAreaPlugin,
1314
} from '@grafana/ui';
1415
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
1516
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
@@ -117,6 +118,7 @@ export const StatusHistoryPanel = ({
117118
{cursorSync !== DashboardCursorSync.Off && (
118119
<EventBusPlugin config={builder} eventBus={eventBus} frame={alignedFrame} />
119120
)}
121+
<XAxisInteractionAreaPlugin config={builder} queryZoom={onChangeTimeRange} />
120122
{options.tooltip.mode !== TooltipDisplayMode.None && (
121123
<TooltipPlugin2
122124
config={builder}

0 commit comments

Comments
 (0)