From 09bf209afdd3d0f30d23f77ea28ab0d8157c885d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 20 May 2026 18:21:03 +0100 Subject: [PATCH] Use canonical ProgressBar for backup row metric bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Backups Size and Duration columns were rendering a bespoke `InlineProgressBar` — a small fixed-width bar floating next to text. Every other row metric in Pulse (Storage usage, Ceph pool usage, Workloads CPU/Memory/Disk) is rendered by `ProgressBar` from `@/components/shared`: full-cell-width fill with the value text overlaid on top, via a foreignObject-clipped label. Introducing a second pattern just for these two columns was a needless divergence called out in the second consistency pass. Drop the bespoke component. Wrap shared `ProgressBar` in a RowMetricBar helper that matches the Storage/Ceph convention (`STORAGE_BAR_LABEL_WRAP_CLASS` shape) and use it for both columns. The Size and Duration headers move to the `metric-bar` column kind so alignment lines up with the bar's natural centring. Bars now share the same shape, animation curve (320ms width transition from `progress-fill`), and label-clipping behaviour as the rest of the app — same code path, same look. --- .../features/proxmox/ProxmoxBackupsTable.tsx | 103 ++++++++---------- 1 file changed, 46 insertions(+), 57 deletions(-) diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx index 35aea0348f..69cbb8012b 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx @@ -14,6 +14,7 @@ import ChevronRightIcon from 'lucide-solid/icons/chevron-right'; import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup'; +import { ProgressBar } from '@/components/shared/ProgressBar'; import { SearchInput } from '@/components/shared/SearchInput'; import { StatusDot } from '@/components/shared/StatusDot'; import { TableCard } from '@/components/shared/TableCard'; @@ -223,33 +224,32 @@ function formatDurationFromSeconds(seconds: number): string { return `${h}h ${m}m`; } -// Compact, table-row-sized horizontal usage bar. Used inline in the -// Backup files Size column (proportional to the largest archive in view) -// and in the Recent tasks Duration column (proportional to 2x the median -// duration so a typical task fills ~50% and outliers fill the bar). -// -// Layout invariant: the bar is positioned AFTER the value text in its -// cell, and the cell uses `justify-end`. Because the bar has a fixed -// width and is the rightmost element, every bar's left and right edges -// land at the same x in every row, so they read as a proper comparison -// chart instead of a row of free-floating segments. -function InlineProgressBar(props: { +// Canonical row metric bar — same shape as Storage's usage bar, Ceph's +// pool usage bar, and Workloads' MetricBar: a full-cell-width +// ProgressBar with the value text overlaid on top of the fill. The +// shared `ProgressBar` primitive (foreignObject-based fill that clips +// the label) is the one source of truth for this pattern in Pulse, so +// the Backups tabs read identically to the rest of the app. +function RowMetricBar(props: { valuePct: number; - toneClass: string; - width?: string; - label?: string; + fillClass: string; + label: string; + tooltip?: string; }) { - const widthClass = props.width ?? 'w-16'; return ( -
- {formatBytes(arc.size)} - 0 - ? (arc.size / archiveSizeMaxBytes()) * 100 - : 0 - } - toneClass={ - archiveSizeMaxBytes() > 0 && - arc.size / archiveSizeMaxBytes() >= 0.66 - ? 'bg-blue-500' - : 'bg-blue-400' - } - label={`${formatBytes(arc.size)} (relative to largest file in view)`} - /> -
+ 0 + ? (arc.size / archiveSizeMaxBytes()) * 100 + : 0 + } + fillClass="bg-blue-500/40 dark:bg-blue-500/40" + label={formatBytes(arc.size)} + tooltip={`${formatBytes(arc.size)} (relative to largest file in view)`} + />
Started - + Duration @@ -1315,22 +1308,18 @@ export const ProxmoxBackupsTable: Component<{ {formatRelativeTime(task.startTime, { compact: true })} -
- - {formatDuration(task.startTime, task.endTime)} - - 0 && durationSec - ? durationBarPct() - : 0 - } - toneClass={durationToneClass()} - label={`Duration ${formatDuration(task.startTime, task.endTime)} (median ${formatDurationFromSeconds(taskDurationBaselineSeconds())})`} - /> -
+ 0 && durationSec + ? durationBarPct() + : 0 + } + fillClass={durationToneClass()} + label={formatDuration(task.startTime, task.endTime)} + tooltip={`Duration ${formatDuration(task.startTime, task.endTime)} (median ${formatDurationFromSeconds(taskDurationBaselineSeconds())})`} + />