Files
manage/frontend/src/components/BackupAlertsTable.tsx
T
Developer 2eb649eceb Mobile Users + Backups tables: stacked cards + selection (Slice 5)
Below md, the Users directory and the three Backups tables render as
MobileCardRow cards:

- UsersPage: display name primary; username/activity/email fields. Each
  card carries a selection checkbox (44px via mobile-touch-target) in the
  actions slot with stopPropagation so toggling selection does not open
  the drawer; card-body tap still opens the drawer.
- BackupAlertsTable: alert message primary; severity/type/created fields;
  Acknowledge action preserved in actions slot.
- BackupJobsTable: job name primary; source/schedule/last-status fields
  (joins latestRuns into a JobCardRow).
- BackupRunsTable: run job_id primary; status/duration/size/started fields;
  status-filter Select renders above both layouts (preserved on mobile).

Desktop (md+) is byte-for-byte identical for all four components -- the
UsersPage diff is dominated by re-indenting the existing Table into the
isMobile ternary else branch.

Fix from Slice 5 review: MobileCardRow now renders the clickable card as
<div role=button tabIndex=0> with Enter/Space keyboard handling instead
of <button>, so nesting a Radix Checkbox (which renders a <button>) in
the actions slot produces valid HTML. The desktop-parity argument for
<button>-in-<button> did not hold (desktop rows are <tr>, not buttons).

Cross-cutting: useIsMobile hardened with typeof window.matchMedia guard
(safe in real browsers; only changes jsdom crash -> false). The file-local
900px compose hook was renamed useComposeViewport to avoid collision with
the shared 768px useIsMobile.

Tests: BackupJobsTable test file added (was untested), UsersPage mobile
selection round-trip + stopPropagation, mobile card render across all
four components. 105 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 5).
2026-06-26 13:24:19 +00:00

123 lines
3.0 KiB
TypeScript

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
MobileCardRow,
type MobileCardField,
} from "@/components/ui/mobile-card";
import { useIsMobile } from "../hooks/useIsMobile";
import type { BackupAlert } from "../types/backups";
interface Props {
alerts: BackupAlert[];
onAcknowledge: (alertId: string) => void;
}
function formatTimestamp(ts: number): string {
return new Date(ts * 1000).toLocaleString();
}
type SeverityVariant = "destructive" | "warning";
/**
* Map an alert severity onto a Badge variant per design §2.3.
* `critical` → destructive (chart-4); `warning` → warning (chart-3).
*/
function severityVariant(severity: string): SeverityVariant {
return severity === "critical" ? "destructive" : "warning";
}
// Mobile card fields (spec R3.2): message is the primary identifier;
// severity/type/created give the at-a-glance info. See OpenSpec change
// `mobile-responsive-parity`, tasks slice 5.2.
const alertCardFields: MobileCardField<BackupAlert>[] = [
{ key: "message", label: "Message", render: (a) => a.message, primary: true },
{
key: "severity",
label: "Severity",
render: (a) => (
<Badge variant={severityVariant(a.severity)}>{a.severity}</Badge>
),
},
{ key: "type", label: "Type", render: (a) => a.alert_type },
{
key: "created",
label: "Created",
render: (a) => formatTimestamp(a.created_at),
},
];
export default function BackupAlertsTable({ alerts, onAcknowledge }: Props) {
const isMobile = useIsMobile();
if (isMobile) {
return (
<MobileCardRow
rows={alerts}
fields={alertCardFields}
getRowId={(a) => a.id}
actions={(a) =>
!a.acknowledged ? (
<Button
size="sm"
variant="outline"
className="mobile-touch-target"
onClick={() => onAcknowledge(a.id)}
>
Ack
</Button>
) : null
}
/>
);
}
return (
<div className="overflow-hidden rounded-lg border border-border">
<Table aria-label="Backup alerts">
<TableHeader>
<TableRow className="bg-card hover:bg-card">
<TableHead>Severity</TableHead>
<TableHead>Type</TableHead>
<TableHead>Message</TableHead>
<TableHead>Created</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{alerts.map((alert) => (
<TableRow key={alert.id}>
<TableCell>
<Badge variant={severityVariant(alert.severity)}>
{alert.severity}
</Badge>
</TableCell>
<TableCell>{alert.alert_type}</TableCell>
<TableCell>{alert.message}</TableCell>
<TableCell>{formatTimestamp(alert.created_at)}</TableCell>
<TableCell>
{!alert.acknowledged && (
<Button
size="sm"
variant="outline"
onClick={() => onAcknowledge(alert.id)}
>
Acknowledge
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}