diff --git a/frontend/src/pages/service-tabs/JobsTab.tsx b/frontend/src/pages/service-tabs/JobsTab.tsx new file mode 100644 index 0000000..e33f525 --- /dev/null +++ b/frontend/src/pages/service-tabs/JobsTab.tsx @@ -0,0 +1,87 @@ +/** + * JobsTab — operational content for the backups service page. + * + * Lifted from the old top-level `components/BackupsPage.tsx`. The three + * sub-tables (Jobs / Runs / Alerts) and their hooks are preserved verbatim. + * + * NOTE: the backup hooks currently query globally (no service_id filter). + * The backend gained `service_id` attribution in Slice 3, but the hooks don't + * yet accept a serviceId param. This tab shows ALL backups data for now; + * per-instance scoping by `instance.id` is a follow-up once the hooks gain the + * parameter. + */ +import { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + useAcknowledgeAlert, + useBackupAlerts, + useBackupJobs, + useBackupRuns, +} from "../../hooks/useBackups"; +import BackupAlertsTable from "../../components/BackupAlertsTable"; +import BackupJobsTable from "../../components/BackupJobsTable"; +import BackupRunsTable from "../../components/BackupRunsTable"; +import type { ServiceInstance } from "../../types"; + +export function JobsTab({ instance }: { instance: ServiceInstance }) { + // instance.id is not yet used — backup hooks query globally (see file + // docstring). Per-instance scoping is a follow-up. + void instance; + const [tab, setTab] = useState("jobs"); + const { data: jobsData, isLoading: jobsLoading } = useBackupJobs(); + const { data: runsData, isLoading: runsLoading } = useBackupRuns(); + const { data: alertsData, isLoading: alertsLoading } = useBackupAlerts( + undefined, + false, + ); + const acknowledgeMutation = useAcknowledgeAlert(); + + // Build a map of latest runs per job + const latestRuns = new Map(); + if (runsData) { + for (const run of runsData) { + const existing = latestRuns.get(run.job_id); + if (!existing || run.started_at > existing.started_at) { + latestRuns.set(run.job_id, run); + } + } + } + + const alertsLabel = alertsData ? `Alerts (${alertsData.length})` : "Alerts"; + + return ( +
+ + + Jobs + Runs + {alertsLabel} + + + {jobsLoading ? ( +

Loading jobs…

+ ) : ( + + )} +
+ + {runsLoading ? ( +

Loading runs…

+ ) : ( + + )} +
+ + {alertsLoading ? ( +

Loading alerts…

+ ) : ( + acknowledgeMutation.mutate(id)} + /> + )} +
+
+
+ ); +} diff --git a/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx b/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx new file mode 100644 index 0000000..dec3b38 --- /dev/null +++ b/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx @@ -0,0 +1,58 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { JobsTab } from "../JobsTab"; +import type { ServiceInstance } from "../../../types"; + +const instance: ServiceInstance = { + id: "bkp-1", + service_type: "backups", + name: "Main Backups", + config: { ingestion_label: "default" }, + secrets_set: {}, + enabled: true, + created_at: 1_700_000_000, + updated_at: 1_700_000_000, +}; + +vi.mock("../../../hooks/useBackups", () => ({ + useBackupJobs: () => ({ + data: [ + { + id: "job-1", + name: "nightly", + source: "/data", + target: "s3://bucket", + schedule_interval_seconds: 86400, + created_at: 1_700_000_000, + }, + ], + isLoading: false, + }), + useBackupRuns: () => ({ + data: [], + isLoading: false, + }), + useBackupAlerts: () => ({ + data: [], + isLoading: false, + }), + useAcknowledgeAlert: () => ({ mutate: vi.fn() }), +})); + +function renderTab() { + return render(); +} + +describe("JobsTab", () => { + it("renders the Jobs, Runs, and Alerts sub-tabs", () => { + renderTab(); + expect(screen.getByRole("tab", { name: "Jobs" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: "Runs" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /Alerts/ })).toBeInTheDocument(); + }); + + it("renders the backup job name in the Jobs tab", () => { + renderTab(); + expect(screen.getByText("nightly")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/service-tabs/index.ts b/frontend/src/pages/service-tabs/index.ts index 3b35b01..882ad1a 100644 --- a/frontend/src/pages/service-tabs/index.ts +++ b/frontend/src/pages/service-tabs/index.ts @@ -8,7 +8,6 @@ import type { ComponentType } from "react"; import type { ServiceInstance } from "../../types"; import { AlertsTab, - JobsTab, LinksTab, MessagingTab, MetricsTab, @@ -19,6 +18,7 @@ import { MediaTab } from "./MediaTab"; import { RequestsTab } from "./RequestsTab"; import { FilesTab } from "./FilesTab"; import { ActionsTab } from "./ActionsTab"; +import { JobsTab } from "./JobsTab"; export type ServiceTabComponent = ComponentType<{ instance: ServiceInstance }>; diff --git a/frontend/src/pages/service-tabs/stubs.tsx b/frontend/src/pages/service-tabs/stubs.tsx index d16fe35..0a111d4 100644 --- a/frontend/src/pages/service-tabs/stubs.tsx +++ b/frontend/src/pages/service-tabs/stubs.tsx @@ -28,10 +28,6 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) { return ; } -export function JobsTab({ instance }: { instance: ServiceInstance }) { - return ; -} - export function UsersTab({ instance }: { instance: ServiceInstance }) { return ; }