Skip to content

Commit 34dc350

Browse files
authored
feat: archive workspace (#145)
* feat: add manage workspaces breadcrumb to creation page * feat: retrieve workspace name * test: add archive workspace msw * feat: archive workspace * test: define route configuration
1 parent 14a9cfe commit 34dc350

File tree

9 files changed

+113
-5
lines changed

9 files changed

+113
-5
lines changed

src/Page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function Page() {
1616
<Route path="/prompt/:id" element={<RouteChat />} />
1717
<Route path="/help/:section" element={<RouteHelp />} />
1818
<Route path="/certificates" element={<RouteCertificates />} />
19-
<Route path="/workspace/:id" element={<RouteWorkspace />} />
19+
<Route path="/workspace/:name" element={<RouteWorkspace />} />
2020
<Route path="/workspaces" element={<RouteWorkspaces />} />
2121
<Route path="/workspace/create" element={<RouteWorkspaceCreation />} />
2222
<Route
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { v1DeleteWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
2+
import { toast } from "@stacklok/ui-kit";
3+
import { useMutation } from "@tanstack/react-query";
4+
import { useNavigate } from "react-router-dom";
5+
6+
export function useArchiveWorkspace() {
7+
const navigate = useNavigate();
8+
return useMutation({
9+
...v1DeleteWorkspaceMutation(),
10+
onSuccess: () => navigate("/workspaces"),
11+
onError: (err) => {
12+
toast.error(err.detail ? `${err.detail}` : "Failed to archive workspace");
13+
},
14+
});
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { render } from "@/lib/test-utils";
2+
import { ArchiveWorkspace } from "../archive-workspace";
3+
import userEvent from "@testing-library/user-event";
4+
import { screen, waitFor } from "@testing-library/react";
5+
6+
const mockNavigate = vi.fn();
7+
const mockToast = vi.fn();
8+
vi.mock("react-router-dom", async () => {
9+
const original =
10+
await vi.importActual<typeof import("react-router-dom")>(
11+
"react-router-dom",
12+
);
13+
return {
14+
...original,
15+
useNavigate: () => mockNavigate,
16+
};
17+
});
18+
19+
vi.mock("@stacklok/ui-kit", async () => {
20+
const original =
21+
await vi.importActual<typeof import("@stacklok/ui-kit")>(
22+
"@stacklok/ui-kit",
23+
);
24+
return {
25+
...original,
26+
toast: { error: () => mockToast },
27+
};
28+
});
29+
30+
test("archive workspace", async () => {
31+
render(<ArchiveWorkspace workspaceName="foo" />);
32+
33+
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
34+
await waitFor(() => expect(mockNavigate).toBeCalled());
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Card, CardBody, Button, Text } from "@stacklok/ui-kit";
2+
import { twMerge } from "tailwind-merge";
3+
import { useArchiveWorkspace } from "../../workspace-system-prompt/hooks/use-archive-workspace";
4+
5+
export function ArchiveWorkspace({
6+
className,
7+
workspaceName,
8+
}: {
9+
workspaceName: string;
10+
className?: string;
11+
}) {
12+
const { mutate, isPending } = useArchiveWorkspace();
13+
14+
return (
15+
<Card className={twMerge(className, "shrink-0")}>
16+
<CardBody>
17+
<Text className="text-primary">Archive Workspace</Text>
18+
<div className="flex justify-between items-center">
19+
<Text className="flex items-center text-secondary mb-0">
20+
Archiving this workspace removes it from the main workspaces list,
21+
though it can be restored if needed.
22+
</Text>
23+
<Button
24+
isPending={isPending}
25+
onPress={() => {
26+
mutate({ path: { workspace_name: workspaceName } });
27+
}}
28+
>
29+
Archive
30+
</Button>
31+
</div>
32+
</CardBody>
33+
</Card>
34+
);
35+
}

src/features/workspace/components/workspace-name.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { Card, CardBody, Input, Label, TextField } from "@stacklok/ui-kit";
22
import { twMerge } from "tailwind-merge";
33

4-
export function WorkspaceName({ className }: { className?: string }) {
4+
export function WorkspaceName({
5+
className,
6+
workspaceName,
7+
}: {
8+
workspaceName: string;
9+
className?: string;
10+
}) {
511
return (
612
<Card className={twMerge(className, "shrink-0")}>
713
<CardBody>
8-
<TextField value="my-awesome-workspace" isReadOnly>
14+
<TextField value={workspaceName} isReadOnly>
915
<Label>Workspace name</Label>
1016
<Input />
1117
</TextField>

src/mocks/msw/handlers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ export const handlers = [
3636
http.post("*/api/v1/workspaces", () => {
3737
return HttpResponse.json(mockedWorkspaces);
3838
}),
39+
http.delete("*/api/v1/workspaces/:workspace_name", () =>
40+
HttpResponse.json({ status: 204 }),
41+
),
3942
];

src/routes/__tests__/route-workspace.test.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { render, within } from "@/lib/test-utils";
22
import { test, expect } from "vitest";
33
import { RouteWorkspace } from "../route-workspace";
44

5-
const renderComponent = () => render(<RouteWorkspace />);
5+
const renderComponent = () =>
6+
render(<RouteWorkspace />, {
7+
routeConfig: {
8+
initialEntries: ["/workspace/foo"],
9+
},
10+
pathConfig: "/workspace/:name",
11+
});
612

713
vi.mock("@monaco-editor/react", () => {
814
const FakeEditor = vi.fn((props) => {

src/routes/route-workspace-creation.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function RouteWorkspaceCreation() {
88
<>
99
<Breadcrumbs>
1010
<BreadcrumbHome />
11+
<Breadcrumb href="/workspaces">Manage Workspaces</Breadcrumb>
1112
<Breadcrumb>Create Workspace</Breadcrumb>
1213
</Breadcrumbs>
1314

src/routes/route-workspace.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
2+
import { ArchiveWorkspace } from "@/features/workspace/components/archive-workspace";
23
import { SystemPromptEditor } from "@/features/workspace-system-prompt/components/system-prompt-editor";
34
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
45
import { WorkspaceName } from "@/features/workspace/components/workspace-name";
56
import { Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit";
7+
import { useParams } from "react-router-dom";
68

79
export function RouteWorkspace() {
10+
const { name } = useParams();
11+
12+
if (!name) throw Error("Workspace name is required");
13+
814
return (
915
<>
1016
<Breadcrumbs>
@@ -14,8 +20,9 @@ export function RouteWorkspace() {
1420
</Breadcrumbs>
1521

1622
<WorkspaceHeading title="Workspace settings" />
17-
<WorkspaceName className="mb-4" />
23+
<WorkspaceName className="mb-4" workspaceName={name} />
1824
<SystemPromptEditor className="mb-4" />
25+
<ArchiveWorkspace workspaceName={name} />
1926
</>
2027
);
2128
}

0 commit comments

Comments
 (0)