mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Reduce the Workloads Avail and Backup columns to their signal
Both columns spent permanent width rendering the healthy state, so a fleet with nothing wrong read as two columns of identical repeated values. Avail printed a freshness suffix on every probed row. Freshness is Pulse vouching for its own probe pipeline rather than a fact about the guest, and it reads fresh on almost every row, so it now renders only when the probe data is stale or of unknown age. Avail also becomes toggleable. It was the only permanently pinned column in the table, which made operators who run no availability probes pay 56px for mostly empty cells with no way to hide them. Backup drew a bordered, filled pill with a compact age on every guest. The shield colour already carries the verdict and the configured threshold already decides what counts as acceptable, so the age was decoration on the rows nobody has to act on. A fresh backup is now a bare green shield, and the age, the pill chrome and the alerting colours are reserved for stale, critical and never. The exact timestamp stays one hover away in the tooltip and the age stays in the aria-label, so nothing is lost for assistive tech. Verified against the live backend at 1440x900 and 390x844 across 34 guests. Contract-Neutral: Workloads guest-row presentation only. GuestRowCells.tsx and guestRowModel.tsx are the contracted owners for cell presentation and column metadata in performance-and-scalability.md and this change extends them in place, keeps the shared workloadGuestPresentation helpers required by frontend-primitives.md, and moves no contract section, width rule, or payload field.
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "de5a1b20df6e825dfc590dee334b8877569a74a2",
|
||||
"verified_at": "2026-08-03T22:40:33Z",
|
||||
"base_sha": "3a24d9694f3bb4062170149d02e0eb8241cf398b",
|
||||
"verified_at": "2026-08-03T23:30:46Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/docker/DockerHostsTable.tsx",
|
||||
"frontend-modern/src/features/docker/DockerPageSurface.tsx"
|
||||
"frontend-modern/src/components/Workloads/GuestRowCells.tsx",
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/docker/DockerHostsTable.tsx": "c4042eb72e027b97761f5cd02ba7021e0d7b3669e05e9db9f52af46ecb5bdbbf",
|
||||
"frontend-modern/src/features/docker/DockerPageSurface.tsx": "f4fe3a202d92efaedf61409a4602340f82411ff01252cc399cff758eff23857f"
|
||||
"frontend-modern/src/components/Workloads/GuestRowCells.tsx": "f34d05f6731bacafb07613ac982ba621865e0f3e1db7baae42cff7ac994917d8",
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "6be3cc7e2958f3bd8ead420dac8467b11941b1037721e881a4f1e85e1f5c8cd1"
|
||||
},
|
||||
"routes": ["/standalone/machines", "/docker/overview"],
|
||||
"routes": [
|
||||
"/proxmox"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
@@ -23,15 +25,19 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Desktop Docker Overview fully loaded with the scoped Docker hosts title and View all machines action",
|
||||
"Desktop Machines inventory after following the View all machines action, showing all five current machines",
|
||||
"Phone-width Docker Overview fully loaded with the scoped title and action aligned without clipping",
|
||||
"Phone-width Machines inventory after following the View all machines action"
|
||||
"Proxmox Overview grouped workload table against the live 7655 backend with 34 guests across delly, minipc and pi",
|
||||
"Avail column populated for probed guests and blank for unprobed guests such as iventoy, ollama and tails-anon",
|
||||
"Backup column with all 34 rows in the fresh state rendering a bare green shield with no drawn age and no pill chrome",
|
||||
"View preferences popover open with the Columns section expanded and the new Avail toggle listed",
|
||||
"Avail column hidden through the new toggle and then restored to the original hidden set"
|
||||
],
|
||||
"interactions": [
|
||||
"Followed View all machines from Docker Overview at desktop width and confirmed navigation to /standalone/machines with all five rows",
|
||||
"Repeated the View all machines handoff at phone width and confirmed the complete machine table rendered",
|
||||
"Inspected the Docker hosts title, action placement, table containment, mobile navigation, and footer at both widths",
|
||||
"Confirmed the browser console reported no warnings or errors during either responsive journey"
|
||||
"Signed in at 1440x900 and read the Proxmox Overview table through DOM assertions on the availability cells and the backup status badges",
|
||||
"Confirmed availability cells render bare latency such as 3ms, 1ms and 5ms with no trailing freshness suffix",
|
||||
"Confirmed all 34 fresh backup badges render empty text, carry the green shield colour, and have no rounded-full pill chrome, while their aria-label still reports the backup age",
|
||||
"Opened View then Columns, clicked the Avail checkbox off, and confirmed the availability header and every availability cell were removed and the preference persisted to workloadsHiddenColumns",
|
||||
"Clicked Avail back on and confirmed workloadsHiddenColumns returned to its original value of aiContext, os, ip",
|
||||
"Resized to 390x844 and confirmed both columns render unchanged with no horizontal document overflow",
|
||||
"Read console messages filtered to errors and found none across both viewports"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -86,18 +86,24 @@ function getBackupAgeBadgeLabel(
|
||||
}
|
||||
|
||||
function getBackupAgeBadgeClass(status: BackupInfo['status']): string {
|
||||
const base =
|
||||
'inline-flex h-5 min-w-[3.25rem] items-center justify-center gap-1 rounded-full border px-1.5 text-[10px] font-semibold leading-none tabular-nums cursor-help';
|
||||
const layout =
|
||||
'inline-flex h-5 min-w-[3.25rem] items-center justify-center gap-1 px-1.5 text-[10px] font-semibold leading-none tabular-nums cursor-help';
|
||||
// A healthy backup only needs its shield. The colour already carries the
|
||||
// verdict and the threshold already decides what counts as acceptable, so the
|
||||
// exact age is decoration on the rows nobody has to act on; it stays in the
|
||||
// tooltip and the aria-label. Age and pill chrome are reserved for the states
|
||||
// that need attention.
|
||||
const pill = `${layout} rounded-full border`;
|
||||
|
||||
switch (status) {
|
||||
case 'fresh':
|
||||
return `${base} border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-950/40 dark:text-green-300`;
|
||||
return `${layout} text-green-600 dark:text-green-400`;
|
||||
case 'stale':
|
||||
return `${base} border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/70 dark:bg-yellow-950/40 dark:text-yellow-300`;
|
||||
return `${pill} border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/70 dark:bg-yellow-950/40 dark:text-yellow-300`;
|
||||
case 'critical':
|
||||
return `${base} border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300`;
|
||||
return `${pill} border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300`;
|
||||
case 'never':
|
||||
return `${base} border-border bg-surface-alt text-muted`;
|
||||
return `${pill} border-border bg-surface-alt text-muted`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +332,7 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
|
||||
aria-label={ariaLabel()}
|
||||
>
|
||||
<svg
|
||||
class="h-3 w-3 flex-shrink-0"
|
||||
class="h-3.5 w-3.5 flex-shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -346,7 +352,9 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
|
||||
<path d="M10 10l4 4M14 10l-4 4" />
|
||||
</Show>
|
||||
</svg>
|
||||
<span>{badgeLabel()}</span>
|
||||
<Show when={info().status !== 'fresh'}>
|
||||
<span>{badgeLabel()}</span>
|
||||
</Show>
|
||||
</span>
|
||||
|
||||
<TooltipPortal when={tip.show()} x={tip.pos().x} y={tip.pos().y}>
|
||||
@@ -421,8 +429,12 @@ function AvailabilityProbeCell(props: { presentation: AvailabilityProbePresentat
|
||||
: result === 'not checked'
|
||||
? 'pending'
|
||||
: result;
|
||||
// 'fresh' is Pulse vouching for its own probe pipeline, not a fact about the
|
||||
// guest. It is true on nearly every row, so printing it turns the column into
|
||||
// repeated noise. Surface freshness only when it is a reason to distrust the
|
||||
// result.
|
||||
const freshness = p().freshnessLabel === 'freshness unknown' ? 'unknown' : p().freshnessLabel;
|
||||
return `${compactResult} · ${freshness}`;
|
||||
return freshness === 'fresh' ? compactResult : `${compactResult} · ${freshness}`;
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { render, cleanup } from '@solidjs/testing-library';
|
||||
|
||||
import { BackupStatusCell } from '../GuestRowCells';
|
||||
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
|
||||
const renderCell = (lastBackup: string | number | null) => {
|
||||
const { container } = render(() => <BackupStatusCell lastBackup={lastBackup} />);
|
||||
const badge = container.querySelector('span[aria-label^="Backup status"]');
|
||||
if (!badge) throw new Error('backup badge not rendered');
|
||||
return badge as HTMLElement;
|
||||
};
|
||||
|
||||
describe('BackupStatusCell', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// The healthy state is the overwhelming majority of rows. It carries the
|
||||
// verdict in the shield colour alone; the age would be repeated decoration.
|
||||
it('renders a bare green shield with no age text when the backup is fresh', () => {
|
||||
const badge = renderCell(Date.now() - 2 * HOUR_MS);
|
||||
|
||||
expect(badge.textContent?.trim()).toBe('');
|
||||
expect(badge.className).toContain('text-green-600');
|
||||
expect(badge.className).not.toContain('rounded-full');
|
||||
expect(badge.className).not.toContain('border');
|
||||
expect(badge.querySelector('svg')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the age available to assistive tech even when it is not drawn', () => {
|
||||
const badge = renderCell(Date.now() - 2 * HOUR_MS);
|
||||
|
||||
expect(badge.getAttribute('aria-label')).toMatch(/^Backup status: fresh, last backup /);
|
||||
});
|
||||
|
||||
it('draws the age and pill chrome when the backup is stale', () => {
|
||||
const badge = renderCell(Date.now() - 48 * HOUR_MS);
|
||||
|
||||
expect(badge.textContent?.trim()).not.toBe('');
|
||||
expect(badge.className).toContain('rounded-full');
|
||||
expect(badge.className).toContain('border-yellow-200');
|
||||
});
|
||||
|
||||
it('draws the age and pill chrome when the backup is critical', () => {
|
||||
const badge = renderCell(Date.now() - 200 * HOUR_MS);
|
||||
|
||||
expect(badge.textContent?.trim()).not.toBe('');
|
||||
expect(badge.className).toContain('rounded-full');
|
||||
expect(badge.className).toContain('border-red-200');
|
||||
});
|
||||
|
||||
it('labels a guest that has never been backed up', () => {
|
||||
const badge = renderCell(null);
|
||||
|
||||
expect(badge.textContent?.trim()).toBe('None');
|
||||
expect(badge.className).toContain('rounded-full');
|
||||
expect(badge.getAttribute('aria-label')).toBe('Backup status: never');
|
||||
});
|
||||
});
|
||||
@@ -1337,7 +1337,7 @@ describe('context column for PVE workloads', () => {
|
||||
});
|
||||
|
||||
describe('backup column', () => {
|
||||
it('shows compact age text for supported guests with a backup', () => {
|
||||
it('keeps a fresh backup age accessible without drawing it in the row', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-26T12:00:00Z'));
|
||||
|
||||
@@ -1350,12 +1350,32 @@ describe('backup column', () => {
|
||||
visibleColumnIds: ['name', 'backup'],
|
||||
});
|
||||
|
||||
expect(screen.getByText('5h')).toBeTruthy();
|
||||
// A healthy backup is carried by the shield colour alone. The age stays in
|
||||
// the aria-label and the tooltip so nothing is lost.
|
||||
expect(screen.queryByText('5h')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[aria-label="Backup status: fresh, last backup 5 hours ago"]'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('draws the compact age for supported guests whose backup is stale', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-26T12:00:00Z'));
|
||||
|
||||
const { container } = renderGuestRow({
|
||||
guest: makeGuest({
|
||||
type: 'qemu',
|
||||
workloadType: 'vm',
|
||||
lastBackup: Date.parse('2026-05-24T12:00:00Z'),
|
||||
}),
|
||||
visibleColumnIds: ['name', 'backup'],
|
||||
});
|
||||
|
||||
const badge = container.querySelector('[aria-label^="Backup status: stale"]');
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge?.textContent?.trim()).toMatch(/\d/);
|
||||
});
|
||||
|
||||
it('shows None for supported guests without a backup', () => {
|
||||
renderGuestRow({
|
||||
guest: makeGuest({ type: 'qemu', workloadType: 'vm', lastBackup: 0 }),
|
||||
|
||||
@@ -183,6 +183,7 @@ export const GUEST_COLUMNS: ColumnDef[] = [
|
||||
width: '56px',
|
||||
minWidth: '48px',
|
||||
maxWidth: '64px',
|
||||
toggleable: true,
|
||||
kind: 'badge',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user