Frontend: backups Jobs tab (Slice 7)
Replace the JobsTab stub with a real implementation on the backups service page, lifted from components/BackupsPage.tsx. Renders the Jobs/Runs/Alerts sub-tabs with their existing tables (BackupJobsTable, BackupRunsTable, BackupAlertsTable) and the acknowledge-alert mutation. The backup hooks (useBackupJobs/Runs/Alerts) currently query globally -- 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 documented follow-up once the hooks gain the parameter. The page heading from BackupsPage is dropped (the service page header already shows the instance name + 'Backups' binding). stubs.tsx loses the JobsTab stub; index.ts wires the real component. Tests: JobsTab renders sub-tabs + job-name rows with mocked hooks. 96 tests pass (+2); lint/build green. Refs openspec/changes/services-as-hub-ia/ (spec R2.4, tasks slice 7).
This commit is contained in:
@@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Tabs value={tab} onValueChange={setTab}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="jobs">Jobs</TabsTrigger>
|
||||||
|
<TabsTrigger value="runs">Runs</TabsTrigger>
|
||||||
|
<TabsTrigger value="alerts">{alertsLabel}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="jobs">
|
||||||
|
{jobsLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading jobs…</p>
|
||||||
|
) : (
|
||||||
|
<BackupJobsTable jobs={jobsData ?? []} latestRuns={latestRuns} />
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="runs">
|
||||||
|
{runsLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading runs…</p>
|
||||||
|
) : (
|
||||||
|
<BackupRunsTable runs={runsData ?? []} />
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="alerts">
|
||||||
|
{alertsLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading alerts…</p>
|
||||||
|
) : (
|
||||||
|
<BackupAlertsTable
|
||||||
|
alerts={alertsData ?? []}
|
||||||
|
onAcknowledge={(id) => acknowledgeMutation.mutate(id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(<JobsTab instance={instance} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,6 @@ import type { ComponentType } from "react";
|
|||||||
import type { ServiceInstance } from "../../types";
|
import type { ServiceInstance } from "../../types";
|
||||||
import {
|
import {
|
||||||
AlertsTab,
|
AlertsTab,
|
||||||
JobsTab,
|
|
||||||
LinksTab,
|
LinksTab,
|
||||||
MessagingTab,
|
MessagingTab,
|
||||||
MetricsTab,
|
MetricsTab,
|
||||||
@@ -19,6 +18,7 @@ import { MediaTab } from "./MediaTab";
|
|||||||
import { RequestsTab } from "./RequestsTab";
|
import { RequestsTab } from "./RequestsTab";
|
||||||
import { FilesTab } from "./FilesTab";
|
import { FilesTab } from "./FilesTab";
|
||||||
import { ActionsTab } from "./ActionsTab";
|
import { ActionsTab } from "./ActionsTab";
|
||||||
|
import { JobsTab } from "./JobsTab";
|
||||||
|
|
||||||
export type ServiceTabComponent = ComponentType<{ instance: ServiceInstance }>;
|
export type ServiceTabComponent = ComponentType<{ instance: ServiceInstance }>;
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) {
|
|||||||
return <Stub label="Service overview" instance={instance} />;
|
return <Stub label="Service overview" instance={instance} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JobsTab({ instance }: { instance: ServiceInstance }) {
|
|
||||||
return <Stub label="Backup jobs" instance={instance} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UsersTab({ instance }: { instance: ServiceInstance }) {
|
export function UsersTab({ instance }: { instance: ServiceInstance }) {
|
||||||
return <Stub label="Users" instance={instance} />;
|
return <Stub label="Users" instance={instance} />;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user