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:
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import BackupAlertsTable from "../BackupAlertsTable";
|
||||
import type { BackupAlert } from "../../types/backups";
|
||||
|
||||
function alert(overrides: Partial<BackupAlert> = {}): BackupAlert {
|
||||
return {
|
||||
id: "a1",
|
||||
job_id: "job-1",
|
||||
run_id: null,
|
||||
alert_type: "failed_status",
|
||||
severity: "warning",
|
||||
message: "Run failed",
|
||||
acknowledged: false,
|
||||
resolved_at: null,
|
||||
created_at: 1_700_000_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("BackupAlertsTable", () => {
|
||||
it("maps alert severity onto Badge variants per design §2.3", () => {
|
||||
render(
|
||||
<BackupAlertsTable
|
||||
alerts={[
|
||||
alert({ id: "c", severity: "critical" }),
|
||||
alert({ id: "w", severity: "warning" }),
|
||||
]}
|
||||
onAcknowledge={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("critical").getAttribute("data-variant")).toBe(
|
||||
"destructive",
|
||||
);
|
||||
expect(screen.getByText("warning").getAttribute("data-variant")).toBe(
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onAcknowledge with the alert id when the button is clicked", async () => {
|
||||
const onAcknowledge = vi.fn();
|
||||
render(
|
||||
<BackupAlertsTable
|
||||
alerts={[alert({ id: "ack-me" })]}
|
||||
onAcknowledge={onAcknowledge}
|
||||
/>,
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Acknowledge" }));
|
||||
expect(onAcknowledge).toHaveBeenCalledTimes(1);
|
||||
expect(onAcknowledge).toHaveBeenCalledWith("ack-me");
|
||||
});
|
||||
|
||||
it("hides the acknowledge button for already-acknowledged alerts", () => {
|
||||
render(
|
||||
<BackupAlertsTable
|
||||
alerts={[alert({ id: "done", acknowledged: true })]}
|
||||
onAcknowledge={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "Acknowledge" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import BackupDashboardWidget from "../BackupDashboardWidget";
|
||||
import { useBackupDashboard } from "../../hooks/useBackups";
|
||||
|
||||
// The widget reads from the react-query hook; mocking `useBackupDashboard` lets
|
||||
// us exercise the render paths without a QueryClientProvider or network.
|
||||
vi.mock("../../hooks/useBackups", () => ({
|
||||
useBackupDashboard: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseBackupDashboard = vi.mocked(useBackupDashboard);
|
||||
type DashboardResult = ReturnType<typeof useBackupDashboard>;
|
||||
|
||||
function mockResult(
|
||||
data: DashboardResult["data"],
|
||||
isLoading = false,
|
||||
): DashboardResult {
|
||||
return { data, isLoading } as DashboardResult;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseBackupDashboard.mockReset();
|
||||
});
|
||||
|
||||
describe("BackupDashboardWidget", () => {
|
||||
it("renders the loading state while data is pending", () => {
|
||||
mockUseBackupDashboard.mockReturnValue(mockResult(undefined, true));
|
||||
render(<BackupDashboardWidget />);
|
||||
expect(screen.getByText("Loading…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the backup dashboard stats (jobs / 24h success)", () => {
|
||||
mockUseBackupDashboard.mockReturnValue(
|
||||
mockResult({
|
||||
total_jobs: 4,
|
||||
success_rate_24h: 96,
|
||||
active_alerts: 0,
|
||||
last_failed_at: null,
|
||||
}),
|
||||
);
|
||||
render(<BackupDashboardWidget />);
|
||||
expect(screen.getByText("4")).toBeInTheDocument();
|
||||
expect(screen.getByText("96%")).toBeInTheDocument();
|
||||
expect(screen.getByText("Jobs")).toBeInTheDocument();
|
||||
expect(screen.getByText("24h Success")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a destructive Badge for active alerts and shows last-failed time", () => {
|
||||
mockUseBackupDashboard.mockReturnValue(
|
||||
mockResult({
|
||||
total_jobs: 2,
|
||||
success_rate_24h: 50,
|
||||
active_alerts: 3,
|
||||
last_failed_at: 1_700_000_000,
|
||||
}),
|
||||
);
|
||||
render(<BackupDashboardWidget />);
|
||||
const badge = screen.getByText("3");
|
||||
expect(badge.getAttribute("data-variant")).toBe("destructive");
|
||||
expect(screen.getByText(/Last failed:/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import BackupRunsTable from "../BackupRunsTable";
|
||||
import type { BackupRun } from "../../types/backups";
|
||||
|
||||
function run(overrides: Partial<BackupRun> = {}): BackupRun {
|
||||
return {
|
||||
id: "r1",
|
||||
job_id: "job-1",
|
||||
started_at: 1_700_000_000,
|
||||
ended_at: null,
|
||||
status: "success",
|
||||
bytes_transferred: 2048,
|
||||
duration_ms: 1500,
|
||||
error_message: null,
|
||||
details_json: null,
|
||||
created_at: 1_700_000_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("BackupRunsTable", () => {
|
||||
it("maps run status onto Badge variants per design §2.3", () => {
|
||||
render(
|
||||
<BackupRunsTable
|
||||
runs={[
|
||||
run({ id: "a", status: "success" }),
|
||||
run({ id: "b", status: "failure" }),
|
||||
run({ id: "c", status: "in_progress" }),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("success").getAttribute("data-variant")).toBe(
|
||||
"success",
|
||||
);
|
||||
expect(screen.getByText("failure").getAttribute("data-variant")).toBe(
|
||||
"destructive",
|
||||
);
|
||||
expect(screen.getByText("in_progress").getAttribute("data-variant")).toBe(
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the formatted duration and transferred size", () => {
|
||||
render(
|
||||
<BackupRunsTable
|
||||
runs={[
|
||||
run({
|
||||
id: "fmt",
|
||||
duration_ms: 1500,
|
||||
bytes_transferred: 2048,
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("1.5s")).toBeInTheDocument();
|
||||
expect(screen.getByText("2.0 KB")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user