From fcceff99e2d8d8bdea9d77782ff6f12286a6fd08 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Mon, 17 Nov 2025 06:36:36 -0800 Subject: [PATCH] CandleStick: Add timeRangePan (#113888) * Candlestick: Add timeRangePan * Add tests * Rename test file * Optimize time range pan tests --- .../panels-suite/candlestick.spec.ts | 112 ++++++++++++++++++ .../panel/candlestick/CandlestickPanel.tsx | 2 + 2 files changed, 114 insertions(+) create mode 100644 e2e-playwright/panels-suite/candlestick.spec.ts diff --git a/e2e-playwright/panels-suite/candlestick.spec.ts b/e2e-playwright/panels-suite/candlestick.spec.ts new file mode 100644 index 00000000000..ad29f64ab47 --- /dev/null +++ b/e2e-playwright/panels-suite/candlestick.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'MP-Di9F7k'; + +test.use({ + featureToggles: { + timeRangePan: true, + }, +}); + +test.describe('Panels test: Candlestick X-axis panning', { tag: ['@panels', '@candlestick'] }, () => { + test('x-axis panning functionality', async ({ gotoDashboardPage, page, selectors }) => { + let centerX: number; + let centerY: number; + let initialFromTime: number; + let initialToTime: number; + + const dashboardPage = await test.step('Load dashboard and verify cursor changes to grab', async () => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const candlestickPanel = page.locator('.uplot').first(); + await expect(candlestickPanel, 'panel rendered').toBeVisible(); + + const xAxis = candlestickPanel.locator('.u-axis').first(); + await expect(xAxis, 'x-axis rendered').toBeVisible(); + + await xAxis.hover(); + + const cursorStyle = await xAxis.evaluate((el: HTMLElement) => window.getComputedStyle(el).cursor); + expect(cursorStyle, 'cursor is grab').toBe('grab'); + + return dashboardPage; + }); + + await test.step('Capture initial time range', async () => { + const candlestickPanel = page.locator('.uplot').first(); + const xAxis = candlestickPanel.locator('.u-axis').first(); + + const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField); + const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField); + + const initialFrom = await fromField.inputValue(); + const initialTo = await toField.inputValue(); + initialFromTime = new Date(initialFrom).getTime(); + initialToTime = new Date(initialTo).getTime(); + + await page.keyboard.press('Escape'); + + const axisBox = await xAxis.boundingBox(); + if (!axisBox) { + throw new Error('X-axis bounding box not found'); + } + + centerX = axisBox.x + axisBox.width / 2; + centerY = axisBox.y + axisBox.height / 2; + }); + + await test.step('Drag right pans backward in time', async () => { + await page.mouse.move(centerX, centerY); + await page.mouse.down(); + await page.mouse.move(centerX + 100, centerY); + await page.mouse.up(); + + await page.waitForTimeout(1000); + + const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField); + const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField); + + const afterRightFrom = await fromField.inputValue(); + const afterRightTo = await toField.inputValue(); + const afterRightFromTime = new Date(afterRightFrom).getTime(); + const afterRightToTime = new Date(afterRightTo).getTime(); + + expect(afterRightFromTime, 'panned backward').toBeLessThan(initialFromTime); + expect(afterRightToTime, 'panned backward').toBeLessThan(initialToTime); + + await page.keyboard.press('Escape'); + + initialFromTime = afterRightFromTime; + initialToTime = afterRightToTime; + }); + + await test.step('Drag left pans forward in time', async () => { + await page.mouse.move(centerX, centerY); + await page.mouse.down(); + await page.mouse.move(centerX - 100, centerY); + await page.mouse.up(); + + await page.waitForTimeout(1000); + + const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField); + const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField); + + const afterLeftFrom = await fromField.inputValue(); + const afterLeftTo = await toField.inputValue(); + const afterLeftFromTime = new Date(afterLeftFrom).getTime(); + const afterLeftToTime = new Date(afterLeftTo).getTime(); + + expect(afterLeftFromTime, 'panned forward').toBeGreaterThan(initialFromTime); + expect(afterLeftToTime, 'panned forward').toBeGreaterThan(initialToTime); + }); + }); +}); diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index e566a8445c6..4f5f7a46b23 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -12,6 +12,7 @@ import { KeyboardPlugin, TooltipPlugin2, UPlotConfigBuilder, + XAxisInteractionAreaPlugin, usePanelContext, useTheme2, } from '@grafana/ui'; @@ -276,6 +277,7 @@ export const CandlestickPanel = ({ {cursorSync !== DashboardCursorSync.Off && ( )} + {options.tooltip.mode !== TooltipDisplayMode.None && (