feat(frontend): slice 3 — Backups cluster migration + nav/IA
Web UI rework. - Migrate BackupAlertsTable, BackupJobsTable, BackupRunsTable, BackupsPage, BackupDashboardWidget off @mui (shadcn Table + Badge severity variants: success=chart-2, warning=chart-3, destructive) - App.tsx IA: Backups now top-level nav (DatabaseBackup icon); Media surface primary at /media; /applications -> /media redirect in both route trees (mirrors /monitoring -> /observability) Gate: build + lint + test green.
This commit is contained in:
@@ -1,84 +1,90 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Chip,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { BackupJob, BackupRun } from "../types/backups";
|
||||
|
||||
interface Props {
|
||||
jobs: BackupJob[];
|
||||
latestRuns: Map<string, BackupRun>;
|
||||
jobs: BackupJob[];
|
||||
latestRuns: Map<string, BackupRun>;
|
||||
}
|
||||
|
||||
function formatInterval(seconds: number | null): string {
|
||||
if (!seconds) return "N/A";
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
||||
return `${Math.floor(seconds / 86400)}d`;
|
||||
if (!seconds) return "N/A";
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
||||
return `${Math.floor(seconds / 86400)}d`;
|
||||
}
|
||||
|
||||
function formatTimestamp(ts: number | null): string {
|
||||
if (!ts) return "Never";
|
||||
return new Date(ts * 1000).toLocaleString();
|
||||
if (!ts) return "Never";
|
||||
return new Date(ts * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
type StatusVariant = "success" | "destructive" | "warning" | "secondary";
|
||||
|
||||
/**
|
||||
* Map a job/run status onto a Badge variant per design §2.3:
|
||||
* `success` → success (chart-2); `failure` → destructive (chart-4);
|
||||
* `in_progress` → warning (chart-3); unknown → secondary (neutral accent).
|
||||
*/
|
||||
function statusVariant(status: string): StatusVariant {
|
||||
if (status === "success") return "success";
|
||||
if (status === "failure") return "destructive";
|
||||
if (status === "in_progress") return "warning";
|
||||
return "secondary";
|
||||
}
|
||||
|
||||
export default function BackupJobsTable({ jobs, latestRuns }: Props) {
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>Target</TableCell>
|
||||
<TableCell>Schedule</TableCell>
|
||||
<TableCell>Last Status</TableCell>
|
||||
<TableCell>Last Run</TableCell>
|
||||
<TableCell>Next Expected</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{jobs.map((job) => {
|
||||
const run = latestRuns.get(job.id);
|
||||
const status = run?.status ?? "unknown";
|
||||
const nextExpected = run && job.schedule_interval_seconds
|
||||
? run.started_at + job.schedule_interval_seconds
|
||||
: null;
|
||||
|
||||
return (
|
||||
<TableRow key={job.id} hover>
|
||||
<TableCell>{job.name}</TableCell>
|
||||
<TableCell>{job.source ?? "—"}</TableCell>
|
||||
<TableCell>{job.target ?? "—"}</TableCell>
|
||||
<TableCell>{formatInterval(job.schedule_interval_seconds)}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={status}
|
||||
color={
|
||||
status === "success"
|
||||
? "success"
|
||||
: status === "failure"
|
||||
? "error"
|
||||
: status === "in_progress"
|
||||
? "warning"
|
||||
: "default"
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formatTimestamp(run?.started_at ?? null)}</TableCell>
|
||||
<TableCell>{formatTimestamp(nextExpected)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<Table aria-label="Backup jobs">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-card hover:bg-card">
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Target</TableHead>
|
||||
<TableHead>Schedule</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead>Last Run</TableHead>
|
||||
<TableHead>Next Expected</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{jobs.map((job) => {
|
||||
const run = latestRuns.get(job.id);
|
||||
const status = run?.status ?? "unknown";
|
||||
const nextExpected =
|
||||
run && job.schedule_interval_seconds
|
||||
? run.started_at + job.schedule_interval_seconds
|
||||
: null;
|
||||
|
||||
return (
|
||||
<TableRow key={job.id}>
|
||||
<TableCell>{job.name}</TableCell>
|
||||
<TableCell>{job.source ?? "—"}</TableCell>
|
||||
<TableCell>{job.target ?? "—"}</TableCell>
|
||||
<TableCell>
|
||||
{formatInterval(job.schedule_interval_seconds)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusVariant(status)}>{status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatTimestamp(run?.started_at ?? null)}
|
||||
</TableCell>
|
||||
<TableCell>{formatTimestamp(nextExpected)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user