2eb649eceb
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).
123 lines
3.0 KiB
TypeScript
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>
|
|
);
|
|
}
|