Skip to content

Commit 623f408

Browse files
authored
Merge pull request #13 from elsa-workflows/codex/implement-packages-page
[codex] Implement packages admin page
2 parents f67ed9e + 1f35476 commit 623f408

13 files changed

Lines changed: 597 additions & 6 deletions

File tree

src/Elsa.Catalog.AdminUi/src/app/routes.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RequestStateView } from "@/components/states/RequestStateViews";
44
import { NewSourcePage, EditSourcePage } from "@/features/sources/SourceFormPage";
55
import { SourceDetailsPage } from "@/features/sources/SourceDetailsPage";
66
import { SourcesPage } from "@/features/sources/SourcesPage";
7+
import { PackagesPage } from "@/features/packages/PackagesPage";
78
import { SyncRunDetailsPage } from "@/features/sync-runs/SyncRunDetailsPage";
89
import { SyncRunsPage } from "@/features/sync-runs/SyncRunsPage";
910

@@ -32,7 +33,7 @@ export const router = createBrowserRouter([
3233
{ path: "sources/new", element: <NewSourcePage /> },
3334
{ path: "sources/:sourceId", element: <SourceDetailsPage /> },
3435
{ path: "sources/:sourceId/edit", element: <EditSourcePage /> },
35-
{ path: "packages", element: <PlaceholderPage title="Packages" /> },
36+
{ path: "packages", element: <PackagesPage /> },
3637
{ path: "packages/:packageId", element: <PlaceholderPage title="Package Details" /> },
3738
{ path: "packages/:packageId/versions/:version", element: <PlaceholderPage title="Package Version" /> },
3839
{ path: "sync-runs", element: <SyncRunsPage /> },
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2+
import { render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import { MemoryRouter } from "react-router-dom";
5+
import { afterEach, describe, expect, it, vi } from "vitest";
6+
import { PackagesPage } from "@/features/packages/PackagesPage";
7+
import { packageFixture } from "@/test/fixtures";
8+
9+
function renderPackagesPage(response: unknown, status = 200, initialEntry = "/admin/packages") {
10+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
11+
const url = input.toString();
12+
if (init?.method === "POST") {
13+
return new Response(null, { status: 204 });
14+
}
15+
if (url.endsWith("/api/admin/packages")) {
16+
return new Response(JSON.stringify(response), { status, headers: { "Content-Type": "application/json" } });
17+
}
18+
return new Response(JSON.stringify({ title: "Not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
19+
});
20+
21+
vi.stubGlobal("fetch", fetchMock);
22+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
23+
render(
24+
<QueryClientProvider client={queryClient}>
25+
<MemoryRouter initialEntries={[initialEntry]}>
26+
<PackagesPage />
27+
</MemoryRouter>
28+
</QueryClientProvider>
29+
);
30+
return fetchMock;
31+
}
32+
33+
afterEach(() => {
34+
vi.unstubAllGlobals();
35+
});
36+
37+
describe("PackagesPage", () => {
38+
it("shows loading then empty state", async () => {
39+
renderPackagesPage([]);
40+
41+
expect(screen.getByText("Loading packages")).toBeInTheDocument();
42+
expect(await screen.findByText("No indexed packages")).toBeInTheDocument();
43+
});
44+
45+
it("shows package rows with status and visibility evidence", async () => {
46+
renderPackagesPage([packageFixture]);
47+
48+
expect(await screen.findByRole("link", { name: packageFixture.packageId })).toBeInTheDocument();
49+
expect(screen.getByText("1.0.2")).toBeInTheDocument();
50+
expect(screen.getAllByText("Pending").length).toBeGreaterThan(0);
51+
expect(screen.getByText("Valid")).toBeInTheDocument();
52+
expect(screen.getByText("00000000-0000-0000-0000-000000000001")).toBeInTheDocument();
53+
expect(screen.getByText("Listed")).toBeInTheDocument();
54+
expect(screen.getByText("3")).toBeInTheDocument();
55+
});
56+
57+
it("filters package rows from URL-backed state", async () => {
58+
renderPackagesPage([packageFixture], 200, "/admin/packages?q=missing");
59+
60+
expect(await screen.findByText("No matching packages")).toBeInTheDocument();
61+
});
62+
63+
it("approves selected latest package versions", async () => {
64+
const fetchMock = renderPackagesPage([packageFixture]);
65+
66+
await userEvent.click(await screen.findByRole("checkbox", { name: `Select ${packageFixture.packageId}` }));
67+
await userEvent.click(screen.getByRole("button", { name: "Approve Selected" }));
68+
69+
await waitFor(() => {
70+
expect(fetchMock).toHaveBeenCalledWith(
71+
"/api/admin/packages/Elsa.Persistence.PostgreSql/versions/1.0.2/approve",
72+
expect.objectContaining({ method: "POST" })
73+
);
74+
});
75+
const approveCall = fetchMock.mock.calls.find(([url]) => url.toString().endsWith("/approve"));
76+
expect(approveCall?.[1]).not.toHaveProperty("body");
77+
});
78+
79+
it("requires a rejection reason before rejecting selected versions", async () => {
80+
const fetchMock = renderPackagesPage([packageFixture]);
81+
82+
await userEvent.click(await screen.findByRole("checkbox", { name: `Select ${packageFixture.packageId}` }));
83+
expect(screen.getByRole("button", { name: "Reject Selected" })).toBeDisabled();
84+
85+
await userEvent.type(screen.getByLabelText("Rejection reason"), "Missing manifest details");
86+
await userEvent.click(screen.getByRole("button", { name: "Reject Selected" }));
87+
88+
await waitFor(() => {
89+
expect(fetchMock).toHaveBeenCalledWith(
90+
"/api/admin/packages/Elsa.Persistence.PostgreSql/versions/1.0.2/reject",
91+
expect.objectContaining({ method: "POST" })
92+
);
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)