Skip to content

Commit 1c3e0f0

Browse files
authored
feat: add create workspace (#133)
* feat: add create workspace * test: add workspace selection * fix: rename files * refactor: create workflow heading component * test: add post workflow msw handler * test: add workspace creation * feat: add link button to workspace creation * test: update heading level
1 parent 2c97fd4 commit 1c3e0f0

12 files changed

+175
-17
lines changed

src/App.test.tsx

+34-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { screen, waitFor } from "@testing-library/react";
33
import { describe, expect, it, vi } from "vitest";
44
import App from "./App";
55
import React from "react";
6+
import userEvent from "@testing-library/user-event";
67

78
vi.mock("recharts", async (importOriginal) => {
89
const originalModule = (await importOriginal()) as Record<string, unknown>;
@@ -22,38 +23,63 @@ describe("App", () => {
2223
expect(screen.getByText("Setup")).toBeVisible();
2324
expect(screen.getByRole("banner", { name: "App header" })).toBeVisible();
2425
expect(
25-
screen.getByRole("heading", { name: /codeGate dashboard/i }),
26+
screen.getByRole("heading", { name: /codeGate dashboard/i })
2627
).toBeVisible();
2728
expect(
2829
screen.getByRole("link", {
2930
name: /certificate security/i,
30-
}),
31+
})
3132
).toBeVisible();
3233
expect(
3334
screen.getByRole("link", {
3435
name: /set up in continue/i,
35-
}),
36+
})
3637
).toBeVisible();
3738

3839
expect(
3940
screen.getByRole("link", {
4041
name: /set up in copilot/i,
41-
}),
42+
})
4243
).toBeVisible();
4344
expect(
4445
screen.getByRole("link", {
4546
name: /download/i,
46-
}),
47+
})
4748
).toBeVisible();
4849
expect(
4950
screen.getByRole("link", {
5051
name: /documentation/i,
51-
}),
52+
})
5253
).toBeVisible();
5354
await waitFor(() =>
5455
expect(
55-
screen.getByRole("link", { name: /codeGate dashboard/i }),
56-
).toBeVisible(),
56+
screen.getByRole("link", { name: /codeGate dashboard/i })
57+
).toBeVisible()
58+
);
59+
});
60+
61+
it("should render workspaces dropdown", async () => {
62+
render(<App />);
63+
64+
await waitFor(() =>
65+
expect(
66+
screen.getByRole("link", { name: "CodeGate Dashboard" })
67+
).toBeVisible()
68+
);
69+
70+
const workspaceSelectionButton = screen.getByRole("button", {
71+
name: "Workspace default",
72+
});
73+
await waitFor(() => expect(workspaceSelectionButton).toBeVisible());
74+
75+
await userEvent.click(workspaceSelectionButton);
76+
77+
await waitFor(() =>
78+
expect(
79+
screen.getByRole("option", {
80+
name: /anotherworkspae/i,
81+
})
82+
).toBeVisible()
5783
);
5884
});
5985
});

src/App.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RouteHelp } from "./routes/route-help";
1111
import { RouteChat } from "./routes/route-chat";
1212
import { RouteDashboard } from "./routes/route-dashboard";
1313
import { RouteCertificateSecurity } from "./routes/route-certificate-security";
14+
import { RouteWorkspaceCreation } from "./routes/route-workspace-creation";
1415

1516
function App() {
1617
const { data: prompts, isLoading } = usePromptsData();
@@ -32,6 +33,10 @@ function App() {
3233
<Route path="/certificates" element={<RouteCertificates />} />
3334
<Route path="/workspace/:id" element={<RouteWorkspace />} />
3435
<Route path="/workspaces" element={<RouteWorkspaces />} />
36+
<Route
37+
path="/workspace/create"
38+
element={<RouteWorkspaceCreation />}
39+
/>
3540
<Route
3641
path="/certificates/security"
3742
element={<RouteCertificateSecurity />}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { WorkspaceCreation } from "../workspace-creation";
2+
import { render } from "@/lib/test-utils";
3+
import userEvent from "@testing-library/user-event";
4+
import { screen, waitFor } from "@testing-library/react";
5+
6+
const mockNavigate = vi.fn();
7+
vi.mock("react-router-dom", async () => {
8+
const original =
9+
await vi.importActual<typeof import("react-router-dom")>(
10+
"react-router-dom",
11+
);
12+
return {
13+
...original,
14+
useNavigate: () => mockNavigate,
15+
};
16+
});
17+
18+
test("create workspace", async () => {
19+
render(<WorkspaceCreation />);
20+
21+
expect(screen.getByText(/name/i)).toBeVisible();
22+
23+
screen.logTestingPlaygroundURL();
24+
await userEvent.type(screen.getByRole("textbox"), "workspaceA");
25+
await userEvent.click(screen.getByRole("button", { name: /create/i }));
26+
await waitFor(() => expect(mockNavigate).toBeCalled());
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useCreateWorkspace } from "@/features/workspace/hooks/use-create-workspace";
2+
import {
3+
Button,
4+
Card,
5+
CardBody,
6+
CardFooter,
7+
Input,
8+
Label,
9+
LinkButton,
10+
TextField,
11+
} from "@stacklok/ui-kit";
12+
import { useState } from "react";
13+
14+
export function WorkspaceCreation() {
15+
const [workspaceName, setWorkspaceName] = useState("");
16+
const { mutate, isPending, error } = useCreateWorkspace();
17+
const errorMsg = error?.detail ? `${error?.detail}` : "";
18+
19+
const handleCreateWorkspace = () => {
20+
mutate({ body: { name: workspaceName } });
21+
};
22+
23+
return (
24+
<Card>
25+
<CardBody className="w-full">
26+
<TextField
27+
aria-label="Workspace name"
28+
validationBehavior="aria"
29+
isRequired
30+
onChange={setWorkspaceName}
31+
>
32+
<Label>Name</Label>
33+
<Input value={workspaceName} />
34+
{errorMsg && <div className="p-1 text-red-700">{errorMsg}</div>}
35+
</TextField>
36+
</CardBody>
37+
<CardFooter className="justify-end gap-2">
38+
<LinkButton variant="secondary">Cancel</LinkButton>
39+
<Button
40+
isDisabled={isPending || workspaceName === ""}
41+
onPress={() => handleCreateWorkspace()}
42+
>
43+
Create
44+
</Button>
45+
</CardFooter>
46+
</Card>
47+
);
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Heading } from "@stacklok/ui-kit";
2+
import React from "react";
3+
4+
export function WorkspaceHeading({
5+
title,
6+
children,
7+
}: {
8+
title: string;
9+
children?: React.ReactNode;
10+
}) {
11+
return (
12+
<Heading level={4} className="mb-4 flex items-center justify-between">
13+
{title}
14+
{children}
15+
</Heading>
16+
);
17+
}

src/features/workspace/components/workspaces-selection.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function WorkspacesSelection() {
2525
const { mutateAsync: activateWorkspace } = useActivateWorkspace();
2626

2727
const activeWorkspaceName: string | null =
28-
activeWorkspacesResponse?.workspaces[0]?.name ?? null;
28+
activeWorkspacesResponse?.workspaces?.[0]?.name ?? null;
2929

3030
const [isOpen, setIsOpen] = useState(false);
3131
const [searchWorkspace, setSearchWorkspace] = useState("");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import { useNavigate } from "react-router-dom";
3+
import { v1CreateWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
4+
5+
export function useCreateWorkspace() {
6+
const navigate = useNavigate();
7+
return useMutation({
8+
...v1CreateWorkspaceMutation(),
9+
onSuccess: () => navigate("/workspaces"),
10+
});
11+
}

src/mocks/msw/handlers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ export const handlers = [
3333
http.get("*/api/v1/workspaces", () => {
3434
return HttpResponse.json(mockedWorkspaces);
3535
}),
36+
http.post("*/api/v1/workspaces", () => {
37+
return HttpResponse.json(mockedWorkspaces);
38+
}),
3639
];

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ test("renders title", () => {
2222
const { getByRole } = renderComponent();
2323

2424
expect(
25-
getByRole("heading", { name: "Workspace settings", level: 1 }),
25+
getByRole("heading", { name: "Workspace settings", level: 4 }),
2626
).toBeVisible();
2727
});
2828

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
2+
import { WorkspaceCreation } from "@/features/workspace/components/workspace-creation";
3+
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
4+
import { Breadcrumbs, Breadcrumb } from "@stacklok/ui-kit";
5+
6+
export function RouteWorkspaceCreation() {
7+
return (
8+
<>
9+
<Breadcrumbs>
10+
<BreadcrumbHome />
11+
<Breadcrumb>Create Workspace</Breadcrumb>
12+
</Breadcrumbs>
13+
14+
<WorkspaceHeading title="Create Workspace" />
15+
<WorkspaceCreation />
16+
</>
17+
);
18+
}

src/routes/route-workspace.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
22
import { SystemPromptEditor } from "@/features/workspace-system-prompt/components/system-prompt-editor";
3+
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
34
import { WorkspaceName } from "@/features/workspace/components/workspace-name";
4-
import { Breadcrumb, Breadcrumbs, Heading } from "@stacklok/ui-kit";
5+
import { Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit";
56

67
export function RouteWorkspace() {
78
return (
@@ -12,7 +13,7 @@ export function RouteWorkspace() {
1213
<Breadcrumb>Workspace Settings</Breadcrumb>
1314
</Breadcrumbs>
1415

15-
<Heading level={1}>Workspace settings</Heading>
16+
<WorkspaceHeading title="Workspace settings" />
1617
<WorkspaceName className="mb-4" />
1718
<SystemPromptEditor className="mb-4" />
1819
</>

src/routes/route-workspaces.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1+
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
12
import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces";
23
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
34
import {
45
Breadcrumb,
56
Breadcrumbs,
67
Cell,
78
Column,
8-
Heading,
99
LinkButton,
1010
Row,
1111
Table,
1212
TableBody,
1313
TableHeader,
1414
} from "@stacklok/ui-kit";
15-
import { Settings } from "lucide-react";
15+
import { Settings, SquarePlus } from "lucide-react";
1616

1717
export function RouteWorkspaces() {
1818
const result = useListWorkspaces();
@@ -25,9 +25,11 @@ export function RouteWorkspaces() {
2525
<Breadcrumb>Manage Workspaces</Breadcrumb>
2626
</Breadcrumbs>
2727

28-
<Heading level={1} className="mb-5">
29-
Manage Workspaces
30-
</Heading>
28+
<WorkspaceHeading title="Manage Workspaces">
29+
<LinkButton href="/workspace/create" className="w-fit gap-2">
30+
<SquarePlus /> Create Workspace
31+
</LinkButton>
32+
</WorkspaceHeading>
3133

3234
<Table aria-label="List of workspaces">
3335
<Row>

0 commit comments

Comments
 (0)