Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.

feat: add create workspace #133

Merged
merged 8 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import App from "./App";
import React from "react";
import userEvent from "@testing-library/user-event";

vi.mock("recharts", async (importOriginal) => {
const originalModule = (await importOriginal()) as Record<string, unknown>;
Expand All @@ -22,38 +23,63 @@ describe("App", () => {
expect(screen.getByText("Setup")).toBeVisible();
expect(screen.getByRole("banner", { name: "App header" })).toBeVisible();
expect(
screen.getByRole("heading", { name: /codeGate dashboard/i }),
screen.getByRole("heading", { name: /codeGate dashboard/i })
).toBeVisible();
expect(
screen.getByRole("link", {
name: /certificate security/i,
}),
})
).toBeVisible();
expect(
screen.getByRole("link", {
name: /set up in continue/i,
}),
})
).toBeVisible();

expect(
screen.getByRole("link", {
name: /set up in copilot/i,
}),
})
).toBeVisible();
expect(
screen.getByRole("link", {
name: /download/i,
}),
})
).toBeVisible();
expect(
screen.getByRole("link", {
name: /documentation/i,
}),
})
).toBeVisible();
await waitFor(() =>
expect(
screen.getByRole("link", { name: /codeGate dashboard/i }),
).toBeVisible(),
screen.getByRole("link", { name: /codeGate dashboard/i })
).toBeVisible()
);
});

it("should render workspaces dropdown", async () => {
render(<App />);

await waitFor(() =>
expect(
screen.getByRole("link", { name: "CodeGate Dashboard" })
).toBeVisible()
);

const workspaceSelectionButton = screen.getByRole("button", {
name: "Workspace default",
});
await waitFor(() => expect(workspaceSelectionButton).toBeVisible());

await userEvent.click(workspaceSelectionButton);

await waitFor(() =>
expect(
screen.getByRole("option", {
name: /anotherworkspae/i,
})
).toBeVisible()
);
});
});
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RouteHelp } from "./routes/route-help";
import { RouteChat } from "./routes/route-chat";
import { RouteDashboard } from "./routes/route-dashboard";
import { RouteCertificateSecurity } from "./routes/route-certificate-security";
import { RouteWorkspaceCreation } from "./routes/route-workspace-creation";

function App() {
const { data: prompts, isLoading } = usePromptsData();
Expand All @@ -32,6 +33,10 @@ function App() {
<Route path="/certificates" element={<RouteCertificates />} />
<Route path="/workspace/:id" element={<RouteWorkspace />} />
<Route path="/workspaces" element={<RouteWorkspaces />} />
<Route
path="/workspace/create"
element={<RouteWorkspaceCreation />}
/>
<Route
path="/certificates/security"
element={<RouteCertificateSecurity />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { WorkspaceCreation } from "../workspace-creation";
import { render } from "@/lib/test-utils";
import userEvent from "@testing-library/user-event";
import { screen, waitFor } from "@testing-library/react";

const mockNavigate = vi.fn();
vi.mock("react-router-dom", async () => {
const original =
await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom",
);
return {
...original,
useNavigate: () => mockNavigate,
};
});

test("create workspace", async () => {
render(<WorkspaceCreation />);

expect(screen.getByText(/name/i)).toBeVisible();

screen.logTestingPlaygroundURL();
await userEvent.type(screen.getByRole("textbox"), "workspaceA");
await userEvent.click(screen.getByRole("button", { name: /create/i }));
await waitFor(() => expect(mockNavigate).toBeCalled());
});
48 changes: 48 additions & 0 deletions src/features/workspace/components/workspace-creation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCreateWorkspace } from "@/features/workspace/hooks/use-create-workspace";
import {
Button,
Card,
CardBody,
CardFooter,
Input,
Label,
LinkButton,
TextField,
} from "@stacklok/ui-kit";
import { useState } from "react";

export function WorkspaceCreation() {
const [workspaceName, setWorkspaceName] = useState("");
const { mutate, isPending, error } = useCreateWorkspace();
const errorMsg = error?.detail ? `${error?.detail}` : "";

const handleCreateWorkspace = () => {
mutate({ body: { name: workspaceName } });
};

return (
<Card>
<CardBody className="w-full">
<TextField
aria-label="Workspace name"
validationBehavior="aria"
isRequired
onChange={setWorkspaceName}
>
<Label>Name</Label>
<Input value={workspaceName} />
{errorMsg && <div className="p-1 text-red-700">{errorMsg}</div>}
</TextField>
</CardBody>
<CardFooter className="justify-end gap-2">
<LinkButton variant="secondary">Cancel</LinkButton>
<Button
isDisabled={isPending || workspaceName === ""}
onPress={() => handleCreateWorkspace()}
>
Create
</Button>
</CardFooter>
</Card>
);
}
17 changes: 17 additions & 0 deletions src/features/workspace/components/workspace-heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Heading } from "@stacklok/ui-kit";
import React from "react";

export function WorkspaceHeading({
title,
children,
}: {
title: string;
children?: React.ReactNode;
}) {
return (
<Heading level={4} className="mb-4 flex items-center justify-between">
{title}
{children}
</Heading>
);
}
2 changes: 1 addition & 1 deletion src/features/workspace/components/workspaces-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function WorkspacesSelection() {
const { mutateAsync: activateWorkspace } = useActivateWorkspace();

const activeWorkspaceName: string | null =
activeWorkspacesResponse?.workspaces[0]?.name ?? null;
activeWorkspacesResponse?.workspaces?.[0]?.name ?? null;

const [isOpen, setIsOpen] = useState(false);
const [searchWorkspace, setSearchWorkspace] = useState("");
Expand Down
11 changes: 11 additions & 0 deletions src/features/workspace/hooks/use-create-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { v1CreateWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";

export function useCreateWorkspace() {
const navigate = useNavigate();
return useMutation({
...v1CreateWorkspaceMutation(),
onSuccess: () => navigate("/workspaces"),
});
}
3 changes: 3 additions & 0 deletions src/mocks/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ export const handlers = [
http.get("*/api/v1/workspaces", () => {
return HttpResponse.json(mockedWorkspaces);
}),
http.post("*/api/v1/workspaces", () => {
return HttpResponse.json(mockedWorkspaces);
}),
];
2 changes: 1 addition & 1 deletion src/routes/__tests__/route-workspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test("renders title", () => {
const { getByRole } = renderComponent();

expect(
getByRole("heading", { name: "Workspace settings", level: 1 }),
getByRole("heading", { name: "Workspace settings", level: 4 }),
).toBeVisible();
});

Expand Down
18 changes: 18 additions & 0 deletions src/routes/route-workspace-creation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
import { WorkspaceCreation } from "@/features/workspace/components/workspace-creation";
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
import { Breadcrumbs, Breadcrumb } from "@stacklok/ui-kit";

export function RouteWorkspaceCreation() {
return (
<>
<Breadcrumbs>
<BreadcrumbHome />
<Breadcrumb>Create Workspace</Breadcrumb>
</Breadcrumbs>

<WorkspaceHeading title="Create Workspace" />
<WorkspaceCreation />
</>
);
}
5 changes: 3 additions & 2 deletions src/routes/route-workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
import { SystemPromptEditor } from "@/features/workspace-system-prompt/components/system-prompt-editor";
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
import { WorkspaceName } from "@/features/workspace/components/workspace-name";
import { Breadcrumb, Breadcrumbs, Heading } from "@stacklok/ui-kit";
import { Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit";

export function RouteWorkspace() {
return (
Expand All @@ -12,7 +13,7 @@ export function RouteWorkspace() {
<Breadcrumb>Workspace Settings</Breadcrumb>
</Breadcrumbs>

<Heading level={1}>Workspace settings</Heading>
<WorkspaceHeading title="Workspace settings" />
<WorkspaceName className="mb-4" />
<SystemPromptEditor className="mb-4" />
</>
Expand Down
12 changes: 7 additions & 5 deletions src/routes/route-workspaces.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces";
import { BreadcrumbHome } from "@/components/BreadcrumbHome";
import {
Breadcrumb,
Breadcrumbs,
Cell,
Column,
Heading,
LinkButton,
Row,
Table,
TableBody,
TableHeader,
} from "@stacklok/ui-kit";
import { Settings } from "lucide-react";
import { Settings, SquarePlus } from "lucide-react";

export function RouteWorkspaces() {
const result = useListWorkspaces();
Expand All @@ -25,9 +25,11 @@ export function RouteWorkspaces() {
<Breadcrumb>Manage Workspaces</Breadcrumb>
</Breadcrumbs>

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

<Table aria-label="List of workspaces">
<Row>
Expand Down
Loading