From c3c797227e32aeb7c0e8256e7a77f44240157e0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 12:18:14 +0000 Subject: [PATCH 1/5] RefreshPicker: adapt button width to label content instead of fixed 96px Replace the fixed 96px width with a CSS grid-based approach that renders both the active and alternate label text (e.g. Refresh/Cancel), with the alternate hidden via visibility:hidden. This ensures the button always takes the width of the longest label, preventing layout shifts when toggling between states and supporting variable-length translations. Fixes grafana/grafana#119789 Co-authored-by: Ivan Ortega Alba --- .../src/components/SceneRefreshPicker.tsx | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/scenes/src/components/SceneRefreshPicker.tsx b/packages/scenes/src/components/SceneRefreshPicker.tsx index 21bf1d3fc..702bfe774 100644 --- a/packages/scenes/src/components/SceneRefreshPicker.tsx +++ b/packages/scenes/src/components/SceneRefreshPicker.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Unsubscribable } from 'rxjs'; +import { css } from '@emotion/css'; import { rangeUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; @@ -208,48 +209,87 @@ export class SceneRefreshPicker extends SceneObjectBase }; } +const stableWidthContainerStyles = css({ + display: 'inline-grid', + '> *': { + gridColumn: 1, + gridRow: 1, + }, + '.refresh-picker > button:first-child': { + flex: 1, + }, +}); + export function SceneRefreshPickerRenderer({ model }: SceneComponentProps) { const { refresh, intervals, autoEnabled, autoValue, isOnCanvas, primary, withText } = model.useState(); const isRunning = useQueryControllerState(model); + const refreshText = withText + ? t('grafana-scenes.components.scene-refresh-picker.text-refresh', 'Refresh') + : undefined; + const cancelText = withText + ? t('grafana-scenes.components.scene-refresh-picker.text-cancel', 'Cancel') + : undefined; + let text = refresh === RefreshPicker.autoOption?.value ? autoValue - : withText - ? t('grafana-scenes.components.scene-refresh-picker.text-refresh', 'Refresh') - : undefined; + : refreshText; let tooltip: string | undefined; - let width: string | undefined; if (isRunning) { tooltip = t('grafana-scenes.components.scene-refresh-picker.tooltip-cancel', 'Cancel all queries'); if (withText) { - text = t('grafana-scenes.components.scene-refresh-picker.text-cancel', 'Cancel'); + text = cancelText; } } - if (withText) { - width = '96px'; - } + const pickerProps = { + showAutoInterval: autoEnabled, + value: refresh, + intervals: intervals, + primary: primary, + isOnCanvas: isOnCanvas ?? true, + }; - return ( + const picker = ( { model.onRefresh(); }} - primary={primary} onIntervalChanged={model.onIntervalChanged} isLoading={isRunning} - isOnCanvas={isOnCanvas ?? true} /> ); + + if (!withText) { + return picker; + } + + const alternateText = isRunning + ? (refresh === RefreshPicker.autoOption?.value ? autoValue : refreshText) + : cancelText; + + return ( +
+ +
+ {picker} +
+
+ ); } function useQueryControllerState(model: SceneObject): boolean { From 6958734c390e0199b0d7b325aa18474a0760c728 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 12:25:22 +0000 Subject: [PATCH 2/5] Fix prettier formatting Co-authored-by: Ivan Ortega Alba --- .../src/components/SceneRefreshPicker.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/scenes/src/components/SceneRefreshPicker.tsx b/packages/scenes/src/components/SceneRefreshPicker.tsx index 702bfe774..08367708a 100644 --- a/packages/scenes/src/components/SceneRefreshPicker.tsx +++ b/packages/scenes/src/components/SceneRefreshPicker.tsx @@ -227,14 +227,9 @@ export function SceneRefreshPickerRenderer({ model }: SceneComponentProps -
- {picker} -
+
{picker}
); } From b7dcb69a744aac809e7577e2a080f8af8e3f4c4f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 21:36:30 +0000 Subject: [PATCH 3/5] RefreshPicker: remove fixed 96px width, let button adapt to content Remove the hard-coded width='96px' that was applied when withText is enabled. The button now naturally sizes to its text content, which correctly handles translations of any length without truncation. Co-authored-by: Ivan Ortega Alba --- .../src/components/SceneRefreshPicker.tsx | 67 ++++--------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/packages/scenes/src/components/SceneRefreshPicker.tsx b/packages/scenes/src/components/SceneRefreshPicker.tsx index 08367708a..283f26c23 100644 --- a/packages/scenes/src/components/SceneRefreshPicker.tsx +++ b/packages/scenes/src/components/SceneRefreshPicker.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Unsubscribable } from 'rxjs'; -import { css } from '@emotion/css'; import { rangeUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; import { RefreshPicker } from '@grafana/ui'; @@ -209,82 +208,42 @@ export class SceneRefreshPicker extends SceneObjectBase }; } -const stableWidthContainerStyles = css({ - display: 'inline-grid', - '> *': { - gridColumn: 1, - gridRow: 1, - }, - '.refresh-picker > button:first-child': { - flex: 1, - }, -}); - export function SceneRefreshPickerRenderer({ model }: SceneComponentProps) { const { refresh, intervals, autoEnabled, autoValue, isOnCanvas, primary, withText } = model.useState(); const isRunning = useQueryControllerState(model); - const refreshText = withText - ? t('grafana-scenes.components.scene-refresh-picker.text-refresh', 'Refresh') - : undefined; - const cancelText = withText ? t('grafana-scenes.components.scene-refresh-picker.text-cancel', 'Cancel') : undefined; - - let text = refresh === RefreshPicker.autoOption?.value ? autoValue : refreshText; + let text = + refresh === RefreshPicker.autoOption?.value + ? autoValue + : withText + ? t('grafana-scenes.components.scene-refresh-picker.text-refresh', 'Refresh') + : undefined; let tooltip: string | undefined; if (isRunning) { tooltip = t('grafana-scenes.components.scene-refresh-picker.tooltip-cancel', 'Cancel all queries'); if (withText) { - text = cancelText; + text = t('grafana-scenes.components.scene-refresh-picker.text-cancel', 'Cancel'); } } - const pickerProps = { - showAutoInterval: autoEnabled, - value: refresh, - intervals: intervals, - primary: primary, - isOnCanvas: isOnCanvas ?? true, - }; - - const picker = ( + return ( { model.onRefresh(); }} + primary={primary} onIntervalChanged={model.onIntervalChanged} isLoading={isRunning} + isOnCanvas={isOnCanvas ?? true} /> ); - - if (!withText) { - return picker; - } - - const alternateText = isRunning - ? refresh === RefreshPicker.autoOption?.value - ? autoValue - : refreshText - : cancelText; - - return ( -
- -
{picker}
-
- ); } function useQueryControllerState(model: SceneObject): boolean { From cd71aeae3cf42cf51dba680012196d6a7cfe108f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 21:41:23 +0000 Subject: [PATCH 4/5] RefreshPicker: use two overlapping spans for stable button width Render both the refresh and cancel labels as overlapping spans inside an inline-grid container, toggling visibility based on the running state. This ensures the button always takes the width of the longest label, preventing layout shifts when toggling and adapting to any translation. Removes the fixed 96px width that caused truncation in longer languages. Fixes grafana/grafana#119789 Co-authored-by: Ivan Ortega Alba --- .../src/components/SceneRefreshPicker.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/scenes/src/components/SceneRefreshPicker.tsx b/packages/scenes/src/components/SceneRefreshPicker.tsx index 283f26c23..e246baef3 100644 --- a/packages/scenes/src/components/SceneRefreshPicker.tsx +++ b/packages/scenes/src/components/SceneRefreshPicker.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Unsubscribable } from 'rxjs'; import { rangeUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; @@ -212,29 +212,38 @@ export function SceneRefreshPickerRenderer({ model }: SceneComponentProps + {refreshText} + {cancelText} + + ); + } + return ( { model.onRefresh(); }} From 2157917615c16702f3d787281faeffde44e8f0db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Mar 2026 21:57:12 +0000 Subject: [PATCH 5/5] Simplify: use inline-grid with two overlapping spans for stable width Both labels overlap in the same grid cell via gridArea:'1/1', toggling visibility. The cell width equals the wider label, so the button never changes size when switching states. Replaces the fixed 96px width and works with any translation length. Co-authored-by: Ivan Ortega Alba --- .../src/components/SceneRefreshPicker.tsx | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/scenes/src/components/SceneRefreshPicker.tsx b/packages/scenes/src/components/SceneRefreshPicker.tsx index e246baef3..ca43b0245 100644 --- a/packages/scenes/src/components/SceneRefreshPicker.tsx +++ b/packages/scenes/src/components/SceneRefreshPicker.tsx @@ -212,27 +212,18 @@ export function SceneRefreshPickerRenderer({ model }: SceneComponentProps - {refreshText} - {cancelText} + {refreshLabel} + {cancelLabel} ); } @@ -242,11 +233,12 @@ export function SceneRefreshPickerRenderer({ model }: SceneComponentProps { - model.onRefresh(); - }} + tooltip={ + isRunning ? t('grafana-scenes.components.scene-refresh-picker.tooltip-cancel', 'Cancel all queries') : undefined + } + // @ts-expect-error RefreshPicker types text as string but accepts ReactNode at runtime + text={text} + onRefresh={() => model.onRefresh()} primary={primary} onIntervalChanged={model.onIntervalChanged} isLoading={isRunning}