From a98492a8c24067ccd037e6fa753caf9994558967 Mon Sep 17 00:00:00 2001 From: Alex McGovern <58784948+alex-mcgovern@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:57:25 +0000 Subject: [PATCH 1/7] feat: implement hard delete for workspaces & refactor workspaces table to allow multiple actions (#185) * feat: useKbdShortcuts hook & example implementation * chore: tidy up remnant * feat: useToastMutation hook * chore: remove junk comment * feat: implement `useToastMutation` for workspaces * refactor: `useQueries` to fetch workspaces data * feat: implement "hard delete" from workspaces table * chore: tidy ups * feat: add keyboard tooltip to create workspace * fix(workspaces): add badge to active workspace * test: test workspace actions & hard delete * chore: tidy up test * fix(workspace name): correct message on rename + test * chore: move code around * fix(custom instructions): failing test & rename to match API changes --- src/context/confirm-context.tsx | 86 +++++ .../hooks/use-archive-workspace.ts | 15 - .../hooks/use-set-system-prompt.tsx | 11 - .../__tests__/archive-workspace.test.tsx | 71 +++-- .../table-actions-workspaces.test.tsx | 301 ++++++++++++++++++ .../workspace-custom-instructions.tsx} | 23 +- .../__tests__/workspace-name.test.tsx | 31 ++ .../components/archive-workspace.tsx | 43 ++- .../components/table-actions-workspaces.tsx | 128 ++++++++ .../workspace/components/table-workspaces.tsx | 83 +++++ .../workspace-custom-instructions.tsx} | 70 ++-- .../constants/monaco-theme.ts} | 0 .../hooks/use-archive-workspace-button.tsx | 10 +- .../use-confirm-hard-delete-workspace.tsx | 35 ++ .../hooks/use-invalidate-workspace-queries.ts | 6 +- .../hooks/use-mutation-activate-workspace.ts | 6 +- .../hooks/use-mutation-archive-workspace.ts | 78 ++++- .../hooks/use-mutation-create-workspace.ts | 9 +- .../use-mutation-hard-delete-workspace.ts | 2 +- .../hooks/use-mutation-restore-workspace.ts | 72 ++++- ...tion-set-workspace-custom-instructions.tsx | 23 ++ ...uery-get-workspace-custom-instructions.ts} | 2 +- .../hooks/use-query-list-all-workspaces.ts | 67 ++++ src/hooks/use-confirm.tsx | 29 ++ src/lib/hrefs.ts | 2 + src/lib/test-utils.tsx | 24 +- src/main.tsx | 7 +- src/mocks/msw/handlers.ts | 43 ++- .../__tests__/route-workspaces.test.tsx | 15 +- src/routes/route-workspace.tsx | 5 +- src/routes/route-workspaces.tsx | 126 +------- vitest.setup.ts | 7 + 32 files changed, 1157 insertions(+), 273 deletions(-) create mode 100644 src/context/confirm-context.tsx delete mode 100644 src/features/workspace-system-prompt/hooks/use-archive-workspace.ts delete mode 100644 src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx create mode 100644 src/features/workspace/components/__tests__/table-actions-workspaces.test.tsx rename src/features/{workspace-system-prompt/components/__tests__/system-prompt-editor.test.tsx => workspace/components/__tests__/workspace-custom-instructions.tsx} (77%) create mode 100644 src/features/workspace/components/__tests__/workspace-name.test.tsx create mode 100644 src/features/workspace/components/table-actions-workspaces.tsx create mode 100644 src/features/workspace/components/table-workspaces.tsx rename src/features/{workspace-system-prompt/components/system-prompt-editor.tsx => workspace/components/workspace-custom-instructions.tsx} (76%) rename src/features/{workspace-system-prompt/constants.ts => workspace/constants/monaco-theme.ts} (100%) create mode 100644 src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx create mode 100644 src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx rename src/features/{workspace-system-prompt/hooks/use-get-system-prompt.ts => workspace/hooks/use-query-get-workspace-custom-instructions.ts} (81%) create mode 100644 src/features/workspace/hooks/use-query-list-all-workspaces.ts create mode 100644 src/hooks/use-confirm.tsx diff --git a/src/context/confirm-context.tsx b/src/context/confirm-context.tsx new file mode 100644 index 00000000..ba7fa19c --- /dev/null +++ b/src/context/confirm-context.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogModal, + DialogModalOverlay, + DialogTitle, +} from "@stacklok/ui-kit"; +import type { ReactNode } from "react"; +import { createContext, useState } from "react"; + +type Buttons = { + yes: ReactNode; + no: ReactNode; +}; + +type Config = { + buttons: Buttons; + title?: ReactNode; + isDestructive?: boolean; +}; + +type Question = { + message: ReactNode; + config: Config; + resolve: (value: boolean) => void; +}; + +type ConfirmContextType = { + confirm: (message: ReactNode, config: Config) => Promise; +}; + +export const ConfirmContext = createContext(null); + +export function ConfirmProvider({ children }: { children: ReactNode }) { + const [activeQuestion, setActiveQuestion] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + const handleAnswer = (answer: boolean) => { + if (activeQuestion === null) return; + activeQuestion.resolve(answer); + setIsOpen(false); + }; + + const confirm = (message: ReactNode, config: Config) => { + return new Promise((resolve) => { + setActiveQuestion({ message, config, resolve }); + setIsOpen(true); + }); + }; + + return ( + + {children} + + + + + + {activeQuestion?.config.title} + + {activeQuestion?.message} + +
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/features/workspace-system-prompt/hooks/use-archive-workspace.ts b/src/features/workspace-system-prompt/hooks/use-archive-workspace.ts deleted file mode 100644 index 21e03749..00000000 --- a/src/features/workspace-system-prompt/hooks/use-archive-workspace.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { v1DeleteWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen"; -import { toast } from "@stacklok/ui-kit"; -import { useMutation } from "@tanstack/react-query"; -import { useNavigate } from "react-router-dom"; - -export function useArchiveWorkspace() { - const navigate = useNavigate(); - return useMutation({ - ...v1DeleteWorkspaceMutation(), - onSuccess: () => navigate("/workspaces"), - onError: (err) => { - toast.error(err.detail ? `${err.detail}` : "Failed to archive workspace"); - }, - }); -} diff --git a/src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx b/src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx deleted file mode 100644 index 77905392..00000000 --- a/src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useMutation } from "@tanstack/react-query"; -import { v1SetWorkspaceCustomInstructionsMutation } from "@/api/generated/@tanstack/react-query.gen"; -import { V1GetWorkspaceCustomInstructionsData } from "@/api/generated"; - -export function usePostSystemPrompt( - options: V1GetWorkspaceCustomInstructionsData, -) { - return useMutation({ - ...v1SetWorkspaceCustomInstructionsMutation(options), - }); -} diff --git a/src/features/workspace/components/__tests__/archive-workspace.test.tsx b/src/features/workspace/components/__tests__/archive-workspace.test.tsx index 43b53b07..a66c1417 100644 --- a/src/features/workspace/components/__tests__/archive-workspace.test.tsx +++ b/src/features/workspace/components/__tests__/archive-workspace.test.tsx @@ -1,29 +1,62 @@ import { render } from "@/lib/test-utils"; import { ArchiveWorkspace } from "../archive-workspace"; 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, - }; +import { waitFor } from "@testing-library/react"; + +test("has correct buttons when not archived", async () => { + const { getByRole } = render( + , + ); + + expect(getByRole("button", { name: /archive/i })).toBeVisible(); +}); + +test("has correct buttons when archived", async () => { + const { getByRole } = render( + , + ); + expect(getByRole("button", { name: /restore/i })).toBeVisible(); + expect(getByRole("button", { name: /permanently delete/i })).toBeVisible(); +}); + +test("can archive workspace", async () => { + const { getByText, getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /archive/i })); + + await waitFor(() => { + expect(getByText(/archived "foo-bar" workspace/i)).toBeVisible(); + }); }); -test("archive workspace", async () => { - render(); +test("can restore archived workspace", async () => { + const { getByText, getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /restore/i })); + + await waitFor(() => { + expect(getByText(/restored "foo-bar" workspace/i)).toBeVisible(); + }); +}); + +test("can permanently delete archived workspace", async () => { + const { getByText, getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /permanently delete/i })); + + await waitFor(() => { + expect(getByRole("dialog", { name: /permanently delete/i })).toBeVisible(); + }); - await userEvent.click(screen.getByRole("button", { name: /archive/i })); - await waitFor(() => expect(mockNavigate).toHaveBeenCalledTimes(1)); - expect(mockNavigate).toHaveBeenCalledWith("/workspaces"); + await userEvent.click(getByRole("button", { name: /delete/i })); await waitFor(() => { - expect(screen.getByText(/archived "(.*)" workspace/i)).toBeVisible(); + expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible(); }); }); diff --git a/src/features/workspace/components/__tests__/table-actions-workspaces.test.tsx b/src/features/workspace/components/__tests__/table-actions-workspaces.test.tsx new file mode 100644 index 00000000..c5488fd9 --- /dev/null +++ b/src/features/workspace/components/__tests__/table-actions-workspaces.test.tsx @@ -0,0 +1,301 @@ +import { hrefs } from "@/lib/hrefs"; + +import { waitFor } from "@testing-library/dom"; +import userEvent from "@testing-library/user-event"; + +import { TableActionsWorkspaces } from "../table-actions-workspaces"; +import { render } from "@/lib/test-utils"; + +const mockNavigate = vi.fn(); +vi.mock("react-router-dom", async () => { + const original = + await vi.importActual( + "react-router-dom", + ); + return { + ...original, + useNavigate: () => mockNavigate, + }; +}); + +it("has correct actions for default workspace when not active", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + expect(activate).not.toHaveAttribute("aria-disabled", "true"); + + const edit = getByRole("menuitem", { name: /edit/i }); + expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("default")); + + const archive = getByRole("menuitem", { name: /archive/i }); + expect(archive).toHaveAttribute("aria-disabled", "true"); +}); + +it("has correct actions for default workspace when active", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + expect(activate).toHaveAttribute("aria-disabled", "true"); + + const edit = getByRole("menuitem", { name: /edit/i }); + expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("default")); + + const archive = getByRole("menuitem", { name: /archive/i }); + expect(archive).toHaveAttribute("aria-disabled", "true"); +}); + +it("has correct actions for normal workspace when not active", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + expect(activate).not.toHaveAttribute("aria-disabled", "true"); + + const edit = getByRole("menuitem", { name: /edit/i }); + expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("foo-bar")); + + const archive = getByRole("menuitem", { name: /archive/i }); + expect(archive).not.toHaveAttribute("aria-disabled", "true"); +}); + +it("has correct actions for normal workspace when active", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + expect(activate).toHaveAttribute("aria-disabled", "true"); + + const edit = getByRole("menuitem", { name: /edit/i }); + expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("foo-bar")); + + const archive = getByRole("menuitem", { name: /archive/i }); + expect(archive).toHaveAttribute("aria-disabled", "true"); +}); + +it("has correct actions for archived workspace", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const restore = getByRole("menuitem", { name: /restore/i }); + expect(restore).not.toHaveAttribute("aria-disabled", "true"); + + const hardDelete = getByRole("menuitem", { + name: /permanently delete/i, + }); + expect(hardDelete).not.toHaveAttribute("aria-disabled", "true"); +}); + +it("can activate default workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + await userEvent.click(activate); + + await waitFor(() => { + expect(getByText(/activated "default" workspace/i)).toBeVisible(); + }); +}); + +it("can edit default workspace", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const edit = getByRole("menuitem", { name: /edit/i }); + await userEvent.click(edit); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + hrefs.workspaces.edit("default"), + undefined, + ); + }); +}); + +it("can activate normal workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const activate = getByRole("menuitem", { name: /activate/i }); + await userEvent.click(activate); + + await waitFor(() => { + expect(getByText(/activated "foo-bar" workspace/i)).toBeVisible(); + }); +}); + +it("can edit normal workspace", async () => { + const { getByRole } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + const edit = getByRole("menuitem", { name: /edit/i }); + await userEvent.click(edit); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + hrefs.workspaces.edit("foo-bar"), + undefined, + ); + }); +}); + +it("can archive normal workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + await userEvent.click(getByRole("menuitem", { name: /archive/i })); + + await waitFor(() => { + expect(getByText(/archived "foo-bar" workspace/i)).toBeVisible(); + }); +}); + +it("can restore archived workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + await userEvent.click(getByRole("menuitem", { name: /restore/i })); + + await waitFor(() => { + expect(getByText(/restored "foo-bar" workspace/i)).toBeVisible(); + }); +}); + +it("can permanently delete archived workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + await userEvent.click(getByRole("button", { name: /actions/i })); + + await waitFor(() => { + expect(getByRole("menu")).toBeVisible(); + }); + + await userEvent.click(getByRole("menuitem", { name: /permanently/i })); + + await waitFor(() => { + expect(getByRole("dialog", { name: /permanently delete/i })).toBeVisible(); + }); + + await userEvent.click(getByRole("button", { name: /delete/i })); + + await waitFor(() => { + expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible(); + }); +}); diff --git a/src/features/workspace-system-prompt/components/__tests__/system-prompt-editor.test.tsx b/src/features/workspace/components/__tests__/workspace-custom-instructions.tsx similarity index 77% rename from src/features/workspace-system-prompt/components/__tests__/system-prompt-editor.test.tsx rename to src/features/workspace/components/__tests__/workspace-custom-instructions.tsx index eaa5acf5..cf63e5fb 100644 --- a/src/features/workspace-system-prompt/components/__tests__/system-prompt-editor.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-custom-instructions.tsx @@ -1,11 +1,10 @@ import { render, waitFor } from "@/lib/test-utils"; import { expect, test } from "vitest"; -import { SystemPromptEditor } from "../system-prompt-editor"; + import userEvent from "@testing-library/user-event"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; - -vi.mock("../../lib/post-system-prompt"); +import { WorkspaceCustomInstructions } from "../workspace-custom-instructions"; vi.mock("@monaco-editor/react", () => { const FakeEditor = vi.fn((props) => { @@ -21,16 +20,18 @@ vi.mock("@monaco-editor/react", () => { }); const renderComponent = () => - render(); + render( + , + ); -test("can update system prompt", async () => { +test("can update custom instructions", async () => { server.use( http.get("*/api/v1/workspaces/:name/custom-instructions", () => { return HttpResponse.json({ prompt: "initial prompt from server" }); }), ); - const { getByRole } = renderComponent(); + const { getByRole, getByText } = renderComponent(); await waitFor(() => { expect(getByRole("textbox")).toBeVisible(); @@ -43,14 +44,20 @@ test("can update system prompt", async () => { await userEvent.type(input, "new prompt from test"); expect(input).toHaveTextContent("new prompt from test"); - await userEvent.click(getByRole("button", { name: /Save/i })); - server.use( http.get("*/api/v1/workspaces/:name/custom-instructions", () => { return HttpResponse.json({ prompt: "new prompt from test" }); }), ); + await userEvent.click(getByRole("button", { name: /Save/i })); + + await waitFor(() => { + expect( + getByText(/successfully updated custom instructions/i), + ).toBeVisible(); + }); + await waitFor(() => { expect(input).toHaveTextContent("new prompt from test"); }); diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx new file mode 100644 index 00000000..2d1ba447 --- /dev/null +++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx @@ -0,0 +1,31 @@ +import { test, expect } from "vitest"; +import { WorkspaceName } from "../workspace-name"; +import { render, waitFor } from "@/lib/test-utils"; +import userEvent from "@testing-library/user-event"; + +test("can rename workspace", async () => { + const { getByRole, getByText } = render( + , + ); + + const input = getByRole("textbox", { name: /workspace name/i }); + await userEvent.clear(input); + + await userEvent.type(input, "baz-qux"); + expect(input).toHaveValue("baz-qux"); + + await userEvent.click(getByRole("button", { name: /save/i })); + + await waitFor(() => { + expect(getByText(/renamed workspace to "baz-qux"/i)).toBeVisible(); + }); +}); + +test("can't rename archived workspace", async () => { + const { getByRole } = render( + , + ); + + expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); + expect(getByRole("button", { name: /save/i })).toBeDisabled(); +}); diff --git a/src/features/workspace/components/archive-workspace.tsx b/src/features/workspace/components/archive-workspace.tsx index 61ded26d..a599f847 100644 --- a/src/features/workspace/components/archive-workspace.tsx +++ b/src/features/workspace/components/archive-workspace.tsx @@ -2,6 +2,40 @@ import { Card, CardBody, Button, Text } from "@stacklok/ui-kit"; import { twMerge } from "tailwind-merge"; import { useRestoreWorkspaceButton } from "../hooks/use-restore-workspace-button"; import { useArchiveWorkspaceButton } from "../hooks/use-archive-workspace-button"; +import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace"; +import { useNavigate } from "react-router-dom"; +import { hrefs } from "@/lib/hrefs"; + +const ButtonsUnarchived = ({ workspaceName }: { workspaceName: string }) => { + const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName }); + + return + + ); +}; export function ArchiveWorkspace({ className, @@ -12,9 +46,6 @@ export function ArchiveWorkspace({ className?: string; isArchived: boolean | undefined; }) { - const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName }); - const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName }); - return ( @@ -26,7 +57,11 @@ export function ArchiveWorkspace({ - + + + + + ); +} diff --git a/src/features/workspace/components/table-workspaces.tsx b/src/features/workspace/components/table-workspaces.tsx new file mode 100644 index 00000000..46187e8c --- /dev/null +++ b/src/features/workspace/components/table-workspaces.tsx @@ -0,0 +1,83 @@ +import { + Badge, + Cell, + Column, + Row, + Table, + TableBody, + TableHeader, +} from "@stacklok/ui-kit"; + +import { useListAllWorkspaces } from "../hooks/use-query-list-all-workspaces"; +import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; +import { TableActionsWorkspaces } from "./table-actions-workspaces"; +import { hrefs } from "@/lib/hrefs"; + +function CellName({ + name, + isArchived = false, + isActive = false, +}: { + name: string; + isArchived: boolean; + isActive: boolean; +}) { + if (isArchived) + return ( + + {name} +    + + Archived + + + ); + + if (isActive) + return ( + + {name} +    + + Active + + + ); + + return {name}; +} + +export function TableWorkspaces() { + const { data: workspaces } = useListAllWorkspaces(); + const { data: activeWorkspaceName } = useActiveWorkspaceName(); + + return ( + + + + + Name + + + + + + {workspaces.map((workspace) => ( + + + + + + + ))} + +
+ ); +} diff --git a/src/features/workspace-system-prompt/components/system-prompt-editor.tsx b/src/features/workspace/components/workspace-custom-instructions.tsx similarity index 76% rename from src/features/workspace-system-prompt/components/system-prompt-editor.tsx rename to src/features/workspace/components/workspace-custom-instructions.tsx index 4071bfb2..2d878842 100644 --- a/src/features/workspace-system-prompt/components/system-prompt-editor.tsx +++ b/src/features/workspace/components/workspace-custom-instructions.tsx @@ -17,21 +17,22 @@ import { useMemo, useState, } from "react"; -import { usePostSystemPrompt } from "../hooks/use-set-system-prompt"; -import { Check } from "lucide-react"; + import { twMerge } from "tailwind-merge"; import { V1GetWorkspaceCustomInstructionsData, V1GetWorkspaceCustomInstructionsResponse, V1SetWorkspaceCustomInstructionsData, } from "@/api/generated"; -import { useGetSystemPrompt } from "../hooks/use-get-system-prompt"; + import { QueryCacheNotifyEvent, QueryClient, useQueryClient, } from "@tanstack/react-query"; import { v1GetWorkspaceCustomInstructionsQueryKey } from "@/api/generated/@tanstack/react-query.gen"; +import { useQueryGetWorkspaceCustomInstructions } from "../hooks/use-query-get-workspace-custom-instructions"; +import { useMutationSetWorkspaceCustomInstructions } from "../hooks/use-mutation-set-workspace-custom-instructions"; type DarkModeContextValue = { preference: "dark" | "light" | null; @@ -54,17 +55,6 @@ function inferDarkMode( return "light"; } -function useSavedStatus() { - const [saved, setSaved] = useState(false); - - useEffect(() => { - const id = setTimeout(() => setSaved(false), 2000); - return () => clearTimeout(id); - }, [saved]); - - return { saved, setSaved }; -} - function EditorLoadingUI() { return ( // arbitrary value to match the monaco editor height @@ -75,7 +65,7 @@ function EditorLoadingUI() { ); } -function isGetSystemPromptQuery( +function isGetWorkspaceCustomInstructionsQuery( queryKey: unknown, options: V1GetWorkspaceCustomInstructionsData, ): boolean { @@ -86,7 +76,9 @@ function isGetSystemPromptQuery( ); } -function getPromptFromNotifyEvent(event: QueryCacheNotifyEvent): string | null { +function getCustomInstructionsFromEvent( + event: QueryCacheNotifyEvent, +): string | null { if ("action" in event === false || "data" in event.action === false) return null; return ( @@ -99,7 +91,7 @@ function getPromptFromNotifyEvent(event: QueryCacheNotifyEvent): string | null { ); } -function usePromptValue({ +function useCustomInstructionsValue({ initialValue, options, queryClient, @@ -117,9 +109,12 @@ function usePromptValue({ if ( event.type === "updated" && event.action.type === "success" && - isGetSystemPromptQuery(event.query.options.queryKey, options) + isGetWorkspaceCustomInstructionsQuery( + event.query.options.queryKey, + options, + ) ) { - const prompt: string | null = getPromptFromNotifyEvent(event); + const prompt: string | null = getCustomInstructionsFromEvent(event); if (prompt === value || prompt === null) return; setValue(prompt); @@ -134,7 +129,7 @@ function usePromptValue({ return { value, setValue }; } -export function SystemPromptEditor({ +export function WorkspaceCustomInstructions({ className, workspaceName, isArchived, @@ -156,21 +151,22 @@ export function SystemPromptEditor({ const queryClient = useQueryClient(); - const { data: systemPromptResponse, isPending: isGetPromptPending } = - useGetSystemPrompt(options); - const { mutate, isPending: isMutationPending } = usePostSystemPrompt(options); + const { + data: customInstructionsResponse, + isPending: isCustomInstructionsPending, + } = useQueryGetWorkspaceCustomInstructions(options); + const { mutateAsync, isPending: isMutationPending } = + useMutationSetWorkspaceCustomInstructions(options); - const { setValue, value } = usePromptValue({ - initialValue: systemPromptResponse?.prompt ?? "", + const { setValue, value } = useCustomInstructionsValue({ + initialValue: customInstructionsResponse?.prompt ?? "", options, queryClient, }); - const { saved, setSaved } = useSavedStatus(); - const handleSubmit = useCallback( (value: string) => { - mutate( + mutateAsync( { ...options, body: { prompt: value } }, { onSuccess: () => { @@ -178,24 +174,23 @@ export function SystemPromptEditor({ queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options), refetchType: "all", }); - setSaved(true); }, }, ); }, - [mutate, options, queryClient, setSaved], + [mutateAsync, options, queryClient], ); return ( - Custom prompt + Custom instructions Pass custom instructions to your LLM to augment it's behavior, and save time & tokens.
- {isGetPromptPending ? ( + {isCustomInstructionsPending ? ( ) : ( diff --git a/src/features/workspace-system-prompt/constants.ts b/src/features/workspace/constants/monaco-theme.ts similarity index 100% rename from src/features/workspace-system-prompt/constants.ts rename to src/features/workspace/constants/monaco-theme.ts diff --git a/src/features/workspace/hooks/use-archive-workspace-button.tsx b/src/features/workspace/hooks/use-archive-workspace-button.tsx index d2746510..cc8e0ebe 100644 --- a/src/features/workspace/hooks/use-archive-workspace-button.tsx +++ b/src/features/workspace/hooks/use-archive-workspace-button.tsx @@ -1,7 +1,6 @@ import { Button } from "@stacklok/ui-kit"; import { ComponentProps } from "react"; import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace"; -import { useNavigate } from "react-router-dom"; export function useArchiveWorkspaceButton({ workspaceName, @@ -9,18 +8,11 @@ export function useArchiveWorkspaceButton({ workspaceName: string; }): ComponentProps { const { mutateAsync, isPending } = useMutationArchiveWorkspace(); - const navigate = useNavigate(); return { isPending, isDisabled: isPending, - onPress: () => - mutateAsync( - { path: { workspace_name: workspaceName } }, - { - onSuccess: () => navigate("/workspaces"), - }, - ), + onPress: () => mutateAsync({ path: { workspace_name: workspaceName } }), isDestructive: true, children: "Archive", }; diff --git a/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx b/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx new file mode 100644 index 00000000..120eec16 --- /dev/null +++ b/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx @@ -0,0 +1,35 @@ +import { useConfirm } from "@/hooks/use-confirm"; +import { useCallback } from "react"; +import { useMutationHardDeleteWorkspace } from "./use-mutation-hard-delete-workspace"; + +export function useConfirmHardDeleteWorkspace() { + const { mutateAsync: hardDeleteWorkspace } = useMutationHardDeleteWorkspace(); + + const { confirm } = useConfirm(); + + return useCallback( + async (...params: Parameters) => { + const answer = await confirm( + <> +

Are you sure you want to delete this workspace?

+

+ You will lose any custom instructions, or other configuration.{" "} + This action cannot be undone. +

+ , + { + buttons: { + yes: "Delete", + no: "Cancel", + }, + title: "Permanently delete workspace", + isDestructive: true, + }, + ); + if (answer) { + return hardDeleteWorkspace(...params); + } + }, + [confirm, hardDeleteWorkspace], + ); +} diff --git a/src/features/workspace/hooks/use-invalidate-workspace-queries.ts b/src/features/workspace/hooks/use-invalidate-workspace-queries.ts index 1985e28f..3bb23afa 100644 --- a/src/features/workspace/hooks/use-invalidate-workspace-queries.ts +++ b/src/features/workspace/hooks/use-invalidate-workspace-queries.ts @@ -8,12 +8,12 @@ import { useCallback } from "react"; export function useInvalidateWorkspaceQueries() { const queryClient = useQueryClient(); - const invalidate = useCallback(() => { - queryClient.invalidateQueries({ + const invalidate = useCallback(async () => { + await queryClient.invalidateQueries({ queryKey: v1ListWorkspacesOptions(), refetchType: "all", }); - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: v1ListArchivedWorkspacesQueryKey(), refetchType: "all", }); diff --git a/src/features/workspace/hooks/use-mutation-activate-workspace.ts b/src/features/workspace/hooks/use-mutation-activate-workspace.ts index 3979d188..0e7ac68a 100644 --- a/src/features/workspace/hooks/use-mutation-activate-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-activate-workspace.ts @@ -1,13 +1,13 @@ import { v1ActivateWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen"; import { useToastMutation as useToastMutation } from "@/hooks/use-toast-mutation"; -import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries"; +import { useQueryClient } from "@tanstack/react-query"; export function useMutationActivateWorkspace() { - const invalidate = useInvalidateWorkspaceQueries(); + const queryClient = useQueryClient(); return useToastMutation({ ...v1ActivateWorkspaceMutation(), - onSuccess: () => invalidate(), + onSuccess: () => queryClient.invalidateQueries({ refetchType: "all" }), // Global setting, refetch **everything** successMsg: (variables) => `Activated "${variables.body.name}" workspace`, }); } diff --git a/src/features/workspace/hooks/use-mutation-archive-workspace.ts b/src/features/workspace/hooks/use-mutation-archive-workspace.ts index cd12a702..7dc0267e 100644 --- a/src/features/workspace/hooks/use-mutation-archive-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-archive-workspace.ts @@ -1,13 +1,87 @@ -import { v1DeleteWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen"; +import { + v1DeleteWorkspaceMutation, + v1ListArchivedWorkspacesQueryKey, + v1ListWorkspacesQueryKey, +} from "@/api/generated/@tanstack/react-query.gen"; import { useToastMutation } from "@/hooks/use-toast-mutation"; import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries"; +import { useQueryClient } from "@tanstack/react-query"; +import { + V1ListArchivedWorkspacesResponse, + V1ListWorkspacesResponse, +} from "@/api/generated"; +import { useActiveWorkspaceName } from "./use-active-workspace-name"; export function useMutationArchiveWorkspace() { + const queryClient = useQueryClient(); const invalidate = useInvalidateWorkspaceQueries(); + const { data: activeWorkspaceName } = useActiveWorkspaceName(); return useToastMutation({ ...v1DeleteWorkspaceMutation(), - onSuccess: () => invalidate(), + onMutate: async (variables) => { + // These conditions would cause the archive operation to error + if (variables.path.workspace_name === "default") return; + if (variables.path.workspace_name === activeWorkspaceName) return; + + // Cancel any outgoing refetches + // Prevents the refetch from overwriting the optimistic update + await queryClient.cancelQueries({ + queryKey: v1ListWorkspacesQueryKey(), + }); + await queryClient.cancelQueries({ + queryKey: v1ListArchivedWorkspacesQueryKey(), + }); + + // Snapshot the previous data + const prevWorkspaces = queryClient.getQueryData( + v1ListWorkspacesQueryKey(), + ); + const prevArchivedWorkspaces = queryClient.getQueryData( + v1ListArchivedWorkspacesQueryKey(), + ); + + if (!prevWorkspaces || !prevArchivedWorkspaces) return; + + // Optimistically update values in cache + await queryClient.setQueryData( + v1ListWorkspacesQueryKey(), + (old: V1ListWorkspacesResponse | null) => ({ + workspaces: old + ? [...old.workspaces].filter( + (o) => o.name !== variables.path.workspace_name, + ) + : [], + }), + ); + await queryClient.setQueryData( + v1ListArchivedWorkspacesQueryKey(), + (old: V1ListArchivedWorkspacesResponse | null) => ({ + workspaces: old + ? [...old.workspaces, { name: variables.path.workspace_name }] + : [], + }), + ); + + return { + prevWorkspaces, + prevArchivedWorkspaces, + }; + }, + onSettled: async () => { + await invalidate(); + }, + // Rollback cache updates on error + onError: async (_a, _b, context) => { + queryClient.setQueryData( + v1ListWorkspacesQueryKey(), + context?.prevWorkspaces, + ); + queryClient.setQueryData( + v1ListArchivedWorkspacesQueryKey(), + context?.prevArchivedWorkspaces, + ); + }, successMsg: (variables) => `Archived "${variables.path.workspace_name}" workspace`, }); diff --git a/src/features/workspace/hooks/use-mutation-create-workspace.ts b/src/features/workspace/hooks/use-mutation-create-workspace.ts index 955e2f71..92e35c43 100644 --- a/src/features/workspace/hooks/use-mutation-create-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-create-workspace.ts @@ -7,7 +7,12 @@ export function useMutationCreateWorkspace() { return useToastMutation({ ...v1CreateWorkspaceMutation(), - onSuccess: () => invalidate(), - successMsg: (variables) => `Created "${variables.body.name}" workspace`, + onSuccess: async () => { + await invalidate(); + }, + successMsg: (variables) => + variables.body.rename_to + ? `Renamed workspace to "${variables.body.rename_to}"` + : `Created "${variables.body.name}" workspace`, }); } diff --git a/src/features/workspace/hooks/use-mutation-hard-delete-workspace.ts b/src/features/workspace/hooks/use-mutation-hard-delete-workspace.ts index 9456788b..7ec91fea 100644 --- a/src/features/workspace/hooks/use-mutation-hard-delete-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-hard-delete-workspace.ts @@ -9,6 +9,6 @@ export function useMutationHardDeleteWorkspace() { ...v1HardDeleteWorkspaceMutation(), onSuccess: () => invalidate(), successMsg: (variables) => - `Permanently deleted "${variables.path.name}" workspace`, + `Permanently deleted "${variables.path.workspace_name}" workspace`, }); } diff --git a/src/features/workspace/hooks/use-mutation-restore-workspace.ts b/src/features/workspace/hooks/use-mutation-restore-workspace.ts index 913b7c01..f984a238 100644 --- a/src/features/workspace/hooks/use-mutation-restore-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-restore-workspace.ts @@ -1,13 +1,81 @@ -import { v1RecoverWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen"; +import { + v1ListArchivedWorkspacesQueryKey, + v1ListWorkspacesQueryKey, + v1RecoverWorkspaceMutation, +} from "@/api/generated/@tanstack/react-query.gen"; import { useToastMutation } from "@/hooks/use-toast-mutation"; import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries"; +import { + V1ListWorkspacesResponse, + V1ListArchivedWorkspacesResponse, +} from "@/api/generated"; +import { useQueryClient } from "@tanstack/react-query"; export function useMutationRestoreWorkspace() { const invalidate = useInvalidateWorkspaceQueries(); + const queryClient = useQueryClient(); return useToastMutation({ ...v1RecoverWorkspaceMutation(), - onSuccess: () => invalidate(), + onMutate: async (variables) => { + // Cancel any outgoing refetches + // Prevents the refetch from overwriting the optimistic update + await queryClient.cancelQueries({ + queryKey: v1ListWorkspacesQueryKey(), + }); + await queryClient.cancelQueries({ + queryKey: v1ListArchivedWorkspacesQueryKey(), + }); + + // Snapshot the previous data + const prevWorkspaces = queryClient.getQueryData( + v1ListWorkspacesQueryKey(), + ); + const prevArchivedWorkspaces = queryClient.getQueryData( + v1ListArchivedWorkspacesQueryKey(), + ); + + if (!prevWorkspaces || !prevArchivedWorkspaces) return; + + // Optimistically update values in cache + queryClient.setQueryData( + v1ListArchivedWorkspacesQueryKey(), + (old: V1ListWorkspacesResponse) => ({ + workspaces: [...old.workspaces].filter( + (o) => o.name !== variables.path.workspace_name, + ), + }), + ); + // Optimistically add the workspace to the non-archived list + queryClient.setQueryData( + v1ListWorkspacesQueryKey(), + (old: V1ListArchivedWorkspacesResponse) => ({ + workspaces: [ + ...old.workspaces, + { name: variables.path.workspace_name }, + ], + }), + ); + + return { + prevWorkspaces, + prevArchivedWorkspaces, + }; + }, + onSettled: async () => { + await invalidate(); + }, + // Rollback cache updates on error + onError: async (_a, _b, context) => { + queryClient.setQueryData( + v1ListWorkspacesQueryKey(), + context?.prevWorkspaces, + ); + queryClient.setQueryData( + v1ListArchivedWorkspacesQueryKey(), + context?.prevArchivedWorkspaces, + ); + }, successMsg: (variables) => `Restored "${variables.path.workspace_name}" workspace`, }); diff --git a/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx b/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx new file mode 100644 index 00000000..e1531c12 --- /dev/null +++ b/src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx @@ -0,0 +1,23 @@ +import { + v1GetWorkspaceCustomInstructionsQueryKey, + v1SetWorkspaceCustomInstructionsMutation, +} from "@/api/generated/@tanstack/react-query.gen"; +import { V1GetWorkspaceCustomInstructionsData } from "@/api/generated"; +import { useToastMutation } from "@/hooks/use-toast-mutation"; +import { useQueryClient } from "@tanstack/react-query"; + +export function useMutationSetWorkspaceCustomInstructions( + options: V1GetWorkspaceCustomInstructionsData, +) { + const queryClient = useQueryClient(); + + return useToastMutation({ + ...v1SetWorkspaceCustomInstructionsMutation(options), + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options), + refetchType: "all", + }), + successMsg: "Successfully updated custom instructions", + }); +} diff --git a/src/features/workspace-system-prompt/hooks/use-get-system-prompt.ts b/src/features/workspace/hooks/use-query-get-workspace-custom-instructions.ts similarity index 81% rename from src/features/workspace-system-prompt/hooks/use-get-system-prompt.ts rename to src/features/workspace/hooks/use-query-get-workspace-custom-instructions.ts index ec85e20a..a39f01b2 100644 --- a/src/features/workspace-system-prompt/hooks/use-get-system-prompt.ts +++ b/src/features/workspace/hooks/use-query-get-workspace-custom-instructions.ts @@ -1,7 +1,7 @@ import { v1GetWorkspaceCustomInstructionsOptions } from "@/api/generated/@tanstack/react-query.gen"; import { useQuery } from "@tanstack/react-query"; -export function useGetSystemPrompt(options: { +export function useQueryGetWorkspaceCustomInstructions(options: { path: { workspace_name: string; }; diff --git a/src/features/workspace/hooks/use-query-list-all-workspaces.ts b/src/features/workspace/hooks/use-query-list-all-workspaces.ts new file mode 100644 index 00000000..37a50551 --- /dev/null +++ b/src/features/workspace/hooks/use-query-list-all-workspaces.ts @@ -0,0 +1,67 @@ +import { + DefinedUseQueryResult, + QueryObserverLoadingErrorResult, + QueryObserverLoadingResult, + QueryObserverPendingResult, + QueryObserverRefetchErrorResult, + useQueries, +} from "@tanstack/react-query"; +import { + v1ListArchivedWorkspacesOptions, + v1ListWorkspacesOptions, +} from "@/api/generated/@tanstack/react-query.gen"; +import { + V1ListArchivedWorkspacesResponse, + V1ListWorkspacesResponse, +} from "@/api/generated"; + +type QueryResult = + | DefinedUseQueryResult + | QueryObserverLoadingErrorResult + | QueryObserverLoadingResult + | QueryObserverPendingResult + | QueryObserverRefetchErrorResult; + +type UseQueryDataReturn = [ + QueryResult, + QueryResult, +]; + +const combine = (results: UseQueryDataReturn) => { + const [workspaces, archivedWorkspaces] = results; + + const active = workspaces.data?.workspaces + ? workspaces.data?.workspaces.map( + (i) => ({ ...i, id: `workspace-${i.name}`, isArchived: false }), + [], + ) + : []; + + const archived = archivedWorkspaces.data?.workspaces + ? archivedWorkspaces.data?.workspaces.map( + (i) => ({ ...i, id: `archived-workspace-${i.name}`, isArchived: true }), + [], + ) + : []; + + return { + data: [...active, ...archived], + isPending: results.some((r) => r.isPending), + isFetching: results.some((r) => r.isFetching), + isRefetching: results.some((r) => r.isRefetching), + }; +}; + +export const useListAllWorkspaces = () => { + return useQueries({ + combine, + queries: [ + { + ...v1ListWorkspacesOptions(), + }, + { + ...v1ListArchivedWorkspacesOptions(), + }, + ], + }); +}; diff --git a/src/hooks/use-confirm.tsx b/src/hooks/use-confirm.tsx new file mode 100644 index 00000000..dc1305cc --- /dev/null +++ b/src/hooks/use-confirm.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { ConfirmContext } from "@/context/confirm-context"; +import type { ReactNode } from "react"; +import { useContext } from "react"; + +type Buttons = { + yes: ReactNode; + no: ReactNode; +}; + +type Config = { + buttons: Buttons; + title?: ReactNode; + isDestructive?: boolean; +}; + +export type ConfirmFunction = ( + message: ReactNode, + config: Config, +) => Promise; + +export const useConfirm = () => { + const context = useContext(ConfirmContext); + if (!context) { + throw new Error("useConfirmContext must be used within a ConfirmProvider"); + } + return context; +}; diff --git a/src/lib/hrefs.ts b/src/lib/hrefs.ts index 5c124f3f..cb3b9407 100644 --- a/src/lib/hrefs.ts +++ b/src/lib/hrefs.ts @@ -1,5 +1,7 @@ export const hrefs = { workspaces: { + all: "/workspaces", create: "/workspace/create", + edit: (name: string) => `/workspace/${name}`, }, }; diff --git a/src/lib/test-utils.tsx b/src/lib/test-utils.tsx index d58c2430..8dc22683 100644 --- a/src/lib/test-utils.tsx +++ b/src/lib/test-utils.tsx @@ -1,4 +1,5 @@ import { SidebarProvider } from "@/components/ui/sidebar"; +import { ConfirmProvider } from "@/context/confirm-context"; import { DarkModeProvider, Toaster } from "@stacklok/ui-kit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RenderOptions, render } from "@testing-library/react"; @@ -9,6 +10,7 @@ import { Route, Routes, } from "react-router-dom"; +import { UiKitClientSideRoutingProvider } from "./ui-kit-client-side-routing"; type RoutConfig = { routeConfig?: MemoryRouterProps; @@ -45,15 +47,19 @@ const renderWithProviders = ( render( - - - - {children}} - /> - - + + + + + + {children}} + /> + + + + , ); diff --git a/src/main.tsx b/src/main.tsx index b3d25e3f..bd08502d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -11,6 +11,7 @@ import { client } from "./api/generated/index.ts"; import { QueryClientProvider } from "./components/react-query-provider.tsx"; import { BrowserRouter } from "react-router-dom"; import { UiKitClientSideRoutingProvider } from "./lib/ui-kit-client-side-routing.tsx"; +import { ConfirmProvider } from "./context/confirm-context.tsx"; // Initialize the API client client.setConfig({ @@ -25,8 +26,10 @@ createRoot(document.getElementById("root")!).render( }> - - + + + + diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index fa5f231b..c0c6f231 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -14,13 +14,15 @@ export const handlers = [ ), http.get("*/api/v1/version", () => HttpResponse.json({ status: "healthy" })), http.get("*/api/v1/workspaces/active", () => - HttpResponse.json([ - { - name: "my-awesome-workspace", - is_active: true, - last_updated: new Date(Date.now()).toISOString(), - }, - ]), + HttpResponse.json({ + workspaces: [ + { + name: "my-awesome-workspace", + is_active: true, + last_updated: new Date(Date.now()).toISOString(), + }, + ], + }), ), http.get("*/api/v1/workspaces/:name/messages", () => { return HttpResponse.json(mockedPrompts); @@ -44,16 +46,27 @@ export const handlers = [ http.post("*/api/v1/workspaces", () => { return HttpResponse.json(mockedWorkspaces); }), - http.post("*/api/v1/workspaces/archive/:workspace_name/recover", () => { - HttpResponse.json({ status: 204 }); - }), - http.delete("*/api/v1/workspaces/:name", () => - HttpResponse.json({ status: 204 }), + http.post( + "*/api/v1/workspaces/active", + () => new HttpResponse(null, { status: 204 }), + ), + http.post( + "*/api/v1/workspaces/archive/:workspace_name/recover", + () => new HttpResponse(null, { status: 204 }), + ), + http.delete( + "*/api/v1/workspaces/:name", + () => new HttpResponse(null, { status: 204 }), + ), + http.delete( + "*/api/v1/workspaces/archive/:name", + () => new HttpResponse(null, { status: 204 }), ), http.get("*/api/v1/workspaces/:name/custom-instructions", () => { return HttpResponse.json({ prompt: "foo" }); }), - http.put("*/api/v1/workspaces/:name/custom-instructions", () => { - return HttpResponse.json({}, { status: 204 }); - }), + http.put( + "*/api/v1/workspaces/:name/custom-instructions", + () => new HttpResponse(null, { status: 204 }), + ), ]; diff --git a/src/routes/__tests__/route-workspaces.test.tsx b/src/routes/__tests__/route-workspaces.test.tsx index 608f2f08..37b44cb6 100644 --- a/src/routes/__tests__/route-workspaces.test.tsx +++ b/src/routes/__tests__/route-workspaces.test.tsx @@ -25,9 +25,6 @@ describe("Workspaces page", () => { it("has a table with the correct columns", () => { expect(screen.getByRole("columnheader", { name: /name/i })).toBeVisible(); - expect( - screen.getByRole("columnheader", { name: /configuration/i }), - ).toBeVisible(); }); it("has a row for each workspace", async () => { @@ -43,12 +40,8 @@ describe("Workspaces page", () => { ).toBeVisible(); const firstRow = screen.getByRole("row", { name: /myworkspace/i }); - const firstButton = within(firstRow).getByRole("link", { - name: /settings/i, - }); - expect(firstButton).toBeVisible(); - expect(firstButton).toHaveAttribute("href", "/workspace/myworkspace"); + expect(firstRow).toHaveAttribute("data-href", "/workspace/myworkspace"); }); it("has archived workspace", async () => { @@ -59,11 +52,5 @@ describe("Workspaces page", () => { expect( screen.getByRole("rowheader", { name: /archived_workspace/i }), ).toBeVisible(); - - expect( - screen.getByRole("button", { - name: /restore configuration/i, - }), - ).toBeVisible(); }); }); diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index 816e1684..968a3609 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -1,12 +1,13 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { ArchiveWorkspace } from "@/features/workspace/components/archive-workspace"; -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 { Alert, Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit"; import { useParams } from "react-router-dom"; import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-workspaces"; import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button"; +import { WorkspaceCustomInstructions } from "@/features/workspace/components/workspace-custom-instructions"; function WorkspaceArchivedBanner({ name }: { name: string }) { const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }); @@ -51,7 +52,7 @@ export function RouteWorkspace() { className="mb-4" workspaceName={name} /> - - {name} -    - - Archived - - - ); - - return {name}; -} - -function CellConfiguration({ - name, - isArchived = false, -}: { - name: string; - isArchived?: boolean; -}) { - const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }); - - if (isArchived) { - return ( - - - - ); - } - - return ( - - - - Settings - - - ); -} - export function RouteWorkspaces() { - const { data: availableWorkspaces } = useListWorkspaces(); - const { data: archivedWorkspaces } = useArchivedWorkspaces(); - const workspaces: (Workspace & { isArchived?: boolean })[] = [ - ...(availableWorkspaces?.workspaces ?? []), - ...(archivedWorkspaces?.workspaces.map((item) => ({ - ...item, - isArchived: true, - })) ?? []), - ]; - const navigate = useNavigate(); useKbdShortcuts([["c", () => navigate(hrefs.workspaces.create)]]); @@ -104,37 +27,18 @@ export function RouteWorkspaces() { - - Create Workspace - + + + Create + + + Create a new workspace + C + + - - - - - Name - - - Configuration - - - - - {workspaces.map((workspace) => ( - - - - - ))} - -
+ ); } diff --git a/vitest.setup.ts b/vitest.setup.ts index 7c7db4dd..e2d97f8d 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -65,10 +65,17 @@ afterEach(() => { }); afterAll(() => server.close()); +const SILENCED_MESSAGES = [ + "Not implemented: navigation (except hash changes)", // JSDom issue — can safely be ignored +]; + failOnConsole({ shouldFailOnDebug: false, shouldFailOnError: true, shouldFailOnInfo: false, shouldFailOnLog: false, shouldFailOnWarn: true, + silenceMessage: (message: string) => { + return SILENCED_MESSAGES.some((m) => message.includes(m)); + }, }); From 648a6749d8e3c56e290af7b36c5a9ff749120749 Mon Sep 17 00:00:00 2001 From: Stacklok Bot <140063061+stacklokbot@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:11:17 +0200 Subject: [PATCH 2/7] chore(main): release 0.8.0 (#176) --- CHANGELOG.md | 18 ++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0280a95a..cb90f545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.8.0](https://github.com/stacklok/codegate-ui/compare/v0.7.2...v0.8.0) (2025-01-24) + + +### Features + +* implement hard delete for workspaces & refactor workspaces table to allow multiple actions ([#185](https://github.com/stacklok/codegate-ui/issues/185)) ([a98492a](https://github.com/stacklok/codegate-ui/commit/a98492a8c24067ccd037e6fa753caf9994558967)) +* implement use toast mutation for workspaces ([#184](https://github.com/stacklok/codegate-ui/issues/184)) ([a67b265](https://github.com/stacklok/codegate-ui/commit/a67b26518cd5ef1c3f6106b604d45456ffc03cf4)) +* redirect to conversation from alerts table ([#191](https://github.com/stacklok/codegate-ui/issues/191)) ([646ed5a](https://github.com/stacklok/codegate-ui/commit/646ed5a6b14661b3956dd2685d9065c4c0d110aa)) +* useKbdShortcuts hook & example implementation ([#180](https://github.com/stacklok/codegate-ui/issues/180)) ([0d935a3](https://github.com/stacklok/codegate-ui/commit/0d935a3b263d5c18c86d5ba669554cf3f2e1f3a7)) +* useToastMutation hook ([#183](https://github.com/stacklok/codegate-ui/issues/183)) ([9fe55a5](https://github.com/stacklok/codegate-ui/commit/9fe55a524fa776348fcb6719d625f57aac36d60a)) + + +### Bug Fixes + +* fix small visual glithc in help menu ([#175](https://github.com/stacklok/codegate-ui/issues/175)) ([7031047](https://github.com/stacklok/codegate-ui/commit/70310476e59085c18755dc3eed6d9a2f09523f0f)) +* parsing promptList text and breadcrumb ([#177](https://github.com/stacklok/codegate-ui/issues/177)) ([6da034d](https://github.com/stacklok/codegate-ui/commit/6da034d9ddb028202c14851319e380f61b139473)) +* sort filtered alerts before pagination ([#190](https://github.com/stacklok/codegate-ui/issues/190)) ([d844610](https://github.com/stacklok/codegate-ui/commit/d84461041076f9647ceae93220deb071d1897a17)) + ## [0.7.2](https://github.com/stacklok/codegate-ui/compare/v0.7.1...v0.7.2) (2025-01-22) diff --git a/package-lock.json b/package-lock.json index 4f8c739c..796ffd60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vite-project", - "version": "0.7.2", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vite-project", - "version": "0.7.2", + "version": "0.8.0", "dependencies": { "@hey-api/client-fetch": "^0.6.0", "@monaco-editor/react": "^4.6.0", diff --git a/package.json b/package.json index dbedc5b8..1003a9cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-project", "private": true, - "version": "0.7.2", + "version": "0.8.0", "type": "module", "scripts": { "dev": "vite", From d865730201eaff07e0e1852750803d0a262e5df6 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Fri, 24 Jan 2025 16:47:20 +0100 Subject: [PATCH 3/7] feat: add not_found route (#194) * feat: add not_found route * leftover --- icons/FlipBackward.svg | 3 --- src/Page.test.tsx | 13 ++++++++++ src/Page.tsx | 2 ++ src/components/EmptyState.tsx | 34 +++++++++++++++++++++++++++ src/components/icons/FlipBackward.tsx | 17 -------------- src/components/icons/index.ts | 1 - src/routes/route-not-found.tsx | 22 +++++++++++++++++ 7 files changed, 71 insertions(+), 21 deletions(-) delete mode 100644 icons/FlipBackward.svg create mode 100644 src/Page.test.tsx create mode 100644 src/components/EmptyState.tsx delete mode 100644 src/components/icons/FlipBackward.tsx create mode 100644 src/routes/route-not-found.tsx diff --git a/icons/FlipBackward.svg b/icons/FlipBackward.svg deleted file mode 100644 index c3d42473..00000000 --- a/icons/FlipBackward.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Page.test.tsx b/src/Page.test.tsx new file mode 100644 index 00000000..b01d95cb --- /dev/null +++ b/src/Page.test.tsx @@ -0,0 +1,13 @@ +import { render } from "./lib/test-utils"; +import Page from "./Page"; + +test("render NotFound route", () => { + const { getByText, getByRole } = render(, { + routeConfig: { + initialEntries: ["/fake-route"], + }, + }); + + expect(getByText(/Oops! There's nothing here/i)).toBeVisible(); + expect(getByRole("button", { name: "Home" })).toBeVisible(); +}); diff --git a/src/Page.tsx b/src/Page.tsx index 07799fb0..6daf8a94 100644 --- a/src/Page.tsx +++ b/src/Page.tsx @@ -8,6 +8,7 @@ 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"; +import { RouteNotFound } from "./routes/route-not-found"; export default function Page() { return ( @@ -23,6 +24,7 @@ export default function Page() { path="/certificates/security" element={} /> + } /> ); } diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 00000000..0c8e60bc --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,34 @@ +interface EmptyStateProps { + children?: React.ReactNode; + title?: string | React.ReactNode; + description?: string | React.ReactNode; + illustration?: React.ReactNode; +} + +export function EmptyState({ + illustration, + title, + children, + description, +}: EmptyStateProps) { + return ( + <> +
+ {illustration != null && ( +
+ {illustration} +
+ )} +
+ {title} +
+
+ {description} +
+ {children != null && ( +
{children}
+ )} +
+ + ); +} diff --git a/src/components/icons/FlipBackward.tsx b/src/components/icons/FlipBackward.tsx deleted file mode 100644 index b96b865e..00000000 --- a/src/components/icons/FlipBackward.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { SVGProps } from "react"; -const SvgFlipBackward = (props: SVGProps) => ( - - - -); -export default SvgFlipBackward; diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 2f138811..b1107dfe 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -1,6 +1,5 @@ export { default as Continue } from "./Continue"; export { default as Copilot } from "./Copilot"; export { default as Discord } from "./Discord"; -export { default as FlipBackward } from "./FlipBackward"; export { default as Github } from "./Github"; export { default as Youtube } from "./Youtube"; diff --git a/src/routes/route-not-found.tsx b/src/routes/route-not-found.tsx new file mode 100644 index 00000000..8207343e --- /dev/null +++ b/src/routes/route-not-found.tsx @@ -0,0 +1,22 @@ +import { EmptyState } from "@/components/EmptyState"; +import { IllustrationError, Button } from "@stacklok/ui-kit"; +import { useNavigate } from "react-router-dom"; +import { FlipBackward } from "@untitled-ui/icons-react"; + +export function RouteNotFound() { + const navigate = useNavigate(); + + return ( +
+ } + > + + +
+ ); +} From 05ed2e2c679afb18873e9f405c75ea3311ec4bcb Mon Sep 17 00:00:00 2001 From: Alex McGovern <58784948+alex-mcgovern@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:32:41 +0000 Subject: [PATCH 4/7] chore: enforce use of useToastMutation (#201) --- eslint.config.js | 16 ++++++++++++++-- src/hooks/use-toast-mutation.ts | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6a4c2802..25f88697 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,7 @@ import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; -import tailwindPlugin from 'eslint-plugin-tailwindcss' +import tailwindPlugin from "eslint-plugin-tailwindcss"; export default tseslint.config( { ignores: ["dist"] }, @@ -56,6 +56,18 @@ export default tseslint.config( ], }, ], + "no-restricted-imports": [ + "error", + { + paths: [ + { + importNames: ["useMutation"], + message: "Use the custom `useToastMutation` instead", + name: "@tanstack/react-query", + }, + ], + }, + ], }, - } + }, ); diff --git a/src/hooks/use-toast-mutation.ts b/src/hooks/use-toast-mutation.ts index a087de57..29b8686a 100644 --- a/src/hooks/use-toast-mutation.ts +++ b/src/hooks/use-toast-mutation.ts @@ -1,6 +1,7 @@ import { toast } from "@stacklok/ui-kit"; import { DefaultError, + // eslint-disable-next-line no-restricted-imports useMutation, UseMutationOptions, } from "@tanstack/react-query"; From 9129a5b983a1b54f7d3b5cffc19cce85a955045a Mon Sep 17 00:00:00 2001 From: Alex McGovern <58784948+alex-mcgovern@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:38:08 +0000 Subject: [PATCH 5/7] fix: workspace hard delete nits (#202) --- .../__tests__/archive-workspace.test.tsx | 40 +++++++++++++- .../__tests__/workspace-name.test.tsx | 33 ++++++++++++ .../components/archive-workspace.tsx | 53 +++++++++++++++++-- .../workspace-custom-instructions.tsx | 4 +- .../hooks/use-archive-workspace-button.tsx | 7 ++- .../use-confirm-hard-delete-workspace.tsx | 8 +-- 6 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/features/workspace/components/__tests__/archive-workspace.test.tsx b/src/features/workspace/components/__tests__/archive-workspace.test.tsx index a66c1417..f27d6183 100644 --- a/src/features/workspace/components/__tests__/archive-workspace.test.tsx +++ b/src/features/workspace/components/__tests__/archive-workspace.test.tsx @@ -2,13 +2,16 @@ import { render } from "@/lib/test-utils"; import { ArchiveWorkspace } from "../archive-workspace"; import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; test("has correct buttons when not archived", async () => { - const { getByRole } = render( + const { getByRole, queryByRole } = render( , ); expect(getByRole("button", { name: /archive/i })).toBeVisible(); + expect(queryByRole("button", { name: /contextual help/i })).toBe(null); }); test("has correct buttons when archived", async () => { @@ -60,3 +63,38 @@ test("can permanently delete archived workspace", async () => { expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible(); }); }); + +test("can't archive active workspace", async () => { + server.use( + http.get("*/api/v1/workspaces/active", () => + HttpResponse.json({ + workspaces: [ + { + name: "foo", + is_active: true, + last_updated: new Date(Date.now()).toISOString(), + }, + ], + }), + ), + ); + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole("button", { name: /archive/i })).toBeDisabled(); + expect(getByRole("button", { name: /contextual help/i })).toBeVisible(); + }); +}); + +test("can't archive default workspace", async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole("button", { name: /archive/i })).toBeDisabled(); + expect(getByRole("button", { name: /contextual help/i })).toBeVisible(); + }); +}); diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx index 2d1ba447..d92b25b4 100644 --- a/src/features/workspace/components/__tests__/workspace-name.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx @@ -2,6 +2,8 @@ import { test, expect } from "vitest"; import { WorkspaceName } from "../workspace-name"; import { render, waitFor } from "@/lib/test-utils"; import userEvent from "@testing-library/user-event"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; test("can rename workspace", async () => { const { getByRole, getByText } = render( @@ -29,3 +31,34 @@ test("can't rename archived workspace", async () => { expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); expect(getByRole("button", { name: /save/i })).toBeDisabled(); }); + +test("can't rename active workspace", async () => { + server.use( + http.get("*/api/v1/workspaces/active", () => + HttpResponse.json({ + workspaces: [ + { + name: "foo", + is_active: true, + last_updated: new Date(Date.now()).toISOString(), + }, + ], + }), + ), + ); + const { getByRole } = render( + , + ); + + expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); + expect(getByRole("button", { name: /save/i })).toBeDisabled(); +}); + +test("can't rename default workspace", async () => { + const { getByRole } = render( + , + ); + + expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); + expect(getByRole("button", { name: /save/i })).toBeDisabled(); +}); diff --git a/src/features/workspace/components/archive-workspace.tsx b/src/features/workspace/components/archive-workspace.tsx index a599f847..aedb2fbf 100644 --- a/src/features/workspace/components/archive-workspace.tsx +++ b/src/features/workspace/components/archive-workspace.tsx @@ -1,15 +1,62 @@ -import { Card, CardBody, Button, Text } from "@stacklok/ui-kit"; +import { + Card, + CardBody, + Button, + Text, + TooltipTrigger, + Tooltip, + TooltipInfoButton, +} from "@stacklok/ui-kit"; import { twMerge } from "tailwind-merge"; import { useRestoreWorkspaceButton } from "../hooks/use-restore-workspace-button"; import { useArchiveWorkspaceButton } from "../hooks/use-archive-workspace-button"; import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace"; import { useNavigate } from "react-router-dom"; import { hrefs } from "@/lib/hrefs"; +import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; + +function getContextualText({ + activeWorkspaceName, + workspaceName, +}: { + workspaceName: string; + activeWorkspaceName: string; +}) { + if (workspaceName === activeWorkspaceName) { + return "Cannot archive the active workspace"; + } + if (workspaceName === "default") { + return "Cannot archive the default workspace"; + } + return null; +} + +// NOTE: You can't show a tooltip on a disabled button +// React Aria's recommended approach is https://spectrum.adobe.com/page/contextual-help/ +function ContextualHelp({ workspaceName }: { workspaceName: string }) { + const { data: activeWorkspaceName } = useActiveWorkspaceName(); + if (!activeWorkspaceName) return null; + + const text = getContextualText({ activeWorkspaceName, workspaceName }); + if (!text) return null; + + return ( + + + {text} + + ); +} const ButtonsUnarchived = ({ workspaceName }: { workspaceName: string }) => { const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName }); - return
+ ); }; const ButtonsArchived = ({ workspaceName }: { workspaceName: string }) => { @@ -51,7 +98,7 @@ export function ArchiveWorkspace({
Archive Workspace - + Archiving this workspace removes it from the main workspaces list, though it can be restored if needed. diff --git a/src/features/workspace/components/workspace-custom-instructions.tsx b/src/features/workspace/components/workspace-custom-instructions.tsx index 2d878842..d2387a8b 100644 --- a/src/features/workspace/components/workspace-custom-instructions.tsx +++ b/src/features/workspace/components/workspace-custom-instructions.tsx @@ -186,8 +186,8 @@ export function WorkspaceCustomInstructions({ Custom instructions - Pass custom instructions to your LLM to augment it's behavior, and - save time & tokens. + Pass custom instructions to your LLM to augment its behavior, and save + time & tokens.
{isCustomInstructionsPending ? ( diff --git a/src/features/workspace/hooks/use-archive-workspace-button.tsx b/src/features/workspace/hooks/use-archive-workspace-button.tsx index cc8e0ebe..e6782ed4 100644 --- a/src/features/workspace/hooks/use-archive-workspace-button.tsx +++ b/src/features/workspace/hooks/use-archive-workspace-button.tsx @@ -1,17 +1,22 @@ import { Button } from "@stacklok/ui-kit"; import { ComponentProps } from "react"; import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace"; +import { useActiveWorkspaceName } from "./use-active-workspace-name"; export function useArchiveWorkspaceButton({ workspaceName, }: { workspaceName: string; }): ComponentProps { + const { data: activeWorkspaceName } = useActiveWorkspaceName(); const { mutateAsync, isPending } = useMutationArchiveWorkspace(); return { isPending, - isDisabled: isPending, + isDisabled: + isPending || + workspaceName === activeWorkspaceName || + workspaceName === "default", onPress: () => mutateAsync({ path: { workspace_name: workspaceName } }), isDestructive: true, children: "Archive", diff --git a/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx b/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx index 120eec16..74e6350a 100644 --- a/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx +++ b/src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx @@ -11,10 +11,12 @@ export function useConfirmHardDeleteWorkspace() { async (...params: Parameters) => { const answer = await confirm( <> -

Are you sure you want to delete this workspace?

+

+ Are you sure you want to permanently delete this workspace? +

- You will lose any custom instructions, or other configuration.{" "} - This action cannot be undone. + You will lose all configuration and data associated with this + workspace, like prompt history or alerts.

, { From bddaed04848ebc9a7340facd4fdebe7f78746363 Mon Sep 17 00:00:00 2001 From: Stacklok Bot <140063061+stacklokbot@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:24:29 +0200 Subject: [PATCH 6/7] chore(main): release 0.9.0 (#200) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb90f545..61fef014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.9.0](https://github.com/stacklok/codegate-ui/compare/v0.8.0...v0.9.0) (2025-01-24) + + +### Features + +* add not_found route ([#194](https://github.com/stacklok/codegate-ui/issues/194)) ([d865730](https://github.com/stacklok/codegate-ui/commit/d865730201eaff07e0e1852750803d0a262e5df6)) + + +### Bug Fixes + +* workspace hard delete nits ([#202](https://github.com/stacklok/codegate-ui/issues/202)) ([9129a5b](https://github.com/stacklok/codegate-ui/commit/9129a5b983a1b54f7d3b5cffc19cce85a955045a)) + ## [0.8.0](https://github.com/stacklok/codegate-ui/compare/v0.7.2...v0.8.0) (2025-01-24) diff --git a/package-lock.json b/package-lock.json index 796ffd60..5dafd58b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vite-project", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vite-project", - "version": "0.8.0", + "version": "0.9.0", "dependencies": { "@hey-api/client-fetch": "^0.6.0", "@monaco-editor/react": "^4.6.0", diff --git a/package.json b/package.json index 1003a9cf..077241f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-project", "private": true, - "version": "0.8.0", + "version": "0.9.0", "type": "module", "scripts": { "dev": "vite", From e849194de012fe82adfdf3d576efa6762fcb586d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:26:55 +0100 Subject: [PATCH 7/7] chore(deps): bump @hey-api/client-fetch from 0.6.0 to 0.7.1 (#153) Bumps [@hey-api/client-fetch](https://github.com/hey-api/openapi-ts) from 0.6.0 to 0.7.1. - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/client-fetch@0.6.0...@hey-api/client-fetch@0.7.1) --- updated-dependencies: - dependency-name: "@hey-api/client-fetch" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5dafd58b..ac94dcef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "vite-project", "version": "0.9.0", "dependencies": { - "@hey-api/client-fetch": "^0.6.0", + "@hey-api/client-fetch": "^0.7.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", @@ -1046,9 +1046,9 @@ } }, "node_modules/@hey-api/client-fetch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.6.0.tgz", - "integrity": "sha512-FlhFsVeH8RxJe/nq8xUzxNbiOpe+GadxlD2pfvDyOyLdCTU4o/LRv46ZVWstaW7DgF4nxhI328chy3+AulwVXw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.7.1.tgz", + "integrity": "sha512-cN9SCpED64PjbP4uUuC+TSigYtk4kJ8bNzf9v5Kzmcl9smMUrwzC1kaUug20wThQXhBiKL4ymDHKa8iRwE1bRg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/hey-api" diff --git a/package.json b/package.json index 077241f0..5e00c7d7 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "generate-icons": "npx @svgr/cli --typescript --no-dimensions --replace-attr-values '#2E323A=currentColor' --jsx-runtime automatic --out-dir ./src/components/icons/ -- icons" }, "dependencies": { - "@hey-api/client-fetch": "^0.6.0", + "@hey-api/client-fetch": "^0.7.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0",