diff --git a/src/App.test.tsx b/src/App.test.tsx index 83f745c4..41abf8f9 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -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; @@ -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(); + + 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() ); }); }); diff --git a/src/App.tsx b/src/App.tsx index 21153c5f..ef8d5cb4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(); @@ -32,6 +33,10 @@ function App() { } /> } /> } /> + } + /> } diff --git a/src/features/workspace/components/__tests__/workspace-creation.test.tsx b/src/features/workspace/components/__tests__/workspace-creation.test.tsx new file mode 100644 index 00000000..58d061e5 --- /dev/null +++ b/src/features/workspace/components/__tests__/workspace-creation.test.tsx @@ -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( + "react-router-dom", + ); + return { + ...original, + useNavigate: () => mockNavigate, + }; +}); + +test("create workspace", async () => { + render(); + + 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()); +}); diff --git a/src/features/workspace/components/workspace-creation.tsx b/src/features/workspace/components/workspace-creation.tsx new file mode 100644 index 00000000..150353af --- /dev/null +++ b/src/features/workspace/components/workspace-creation.tsx @@ -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 ( + + + + + + {errorMsg &&
{errorMsg}
} +
+
+ + Cancel + + +
+ ); +} diff --git a/src/features/workspace/components/workspace-heading.tsx b/src/features/workspace/components/workspace-heading.tsx new file mode 100644 index 00000000..880f9959 --- /dev/null +++ b/src/features/workspace/components/workspace-heading.tsx @@ -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 ( + + {title} + {children} + + ); +} diff --git a/src/features/workspace/components/workspaces-selection.tsx b/src/features/workspace/components/workspaces-selection.tsx index aab1971e..58a50269 100644 --- a/src/features/workspace/components/workspaces-selection.tsx +++ b/src/features/workspace/components/workspaces-selection.tsx @@ -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(""); diff --git a/src/features/workspace/hooks/use-create-workspace.ts b/src/features/workspace/hooks/use-create-workspace.ts new file mode 100644 index 00000000..e5eacfa3 --- /dev/null +++ b/src/features/workspace/hooks/use-create-workspace.ts @@ -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"), + }); +} diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index dbefe629..a0b5db3b 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -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); + }), ]; diff --git a/src/routes/__tests__/route-workspace.test.tsx b/src/routes/__tests__/route-workspace.test.tsx index 5d7a301a..30235726 100644 --- a/src/routes/__tests__/route-workspace.test.tsx +++ b/src/routes/__tests__/route-workspace.test.tsx @@ -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(); }); diff --git a/src/routes/route-workspace-creation.tsx b/src/routes/route-workspace-creation.tsx new file mode 100644 index 00000000..31ae5ef6 --- /dev/null +++ b/src/routes/route-workspace-creation.tsx @@ -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 ( + <> + + + Create Workspace + + + + + + ); +} diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index db68a247..c9f0c4a2 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -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 ( @@ -12,7 +13,7 @@ export function RouteWorkspace() { Workspace Settings - Workspace settings + diff --git a/src/routes/route-workspaces.tsx b/src/routes/route-workspaces.tsx index 05612a73..7fddcc37 100644 --- a/src/routes/route-workspaces.tsx +++ b/src/routes/route-workspaces.tsx @@ -1,3 +1,4 @@ +import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { @@ -5,14 +6,13 @@ import { 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(); @@ -25,9 +25,11 @@ export function RouteWorkspaces() { Manage Workspaces - - Manage Workspaces - + + + Create Workspace + +