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

fix: workspace hard delete nits #202

Merged
merged 2 commits into from
Jan 24, 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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
);

expect(getByRole("button", { name: /archive/i })).toBeVisible();
expect(queryByRole("button", { name: /contextual help/i })).toBe(null);
});

test("has correct buttons when archived", async () => {
Expand Down Expand Up @@ -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(
<ArchiveWorkspace workspaceName="foo" isArchived={false} />,
);

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(
<ArchiveWorkspace workspaceName="default" isArchived={false} />,
);

await waitFor(() => {
expect(getByRole("button", { name: /archive/i })).toBeDisabled();
expect(getByRole("button", { name: /contextual help/i })).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
<WorkspaceName workspaceName="foo" isArchived={true} />,
);

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(
<WorkspaceName workspaceName="foo" isArchived={true} />,
);

expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled();
expect(getByRole("button", { name: /save/i })).toBeDisabled();
});
53 changes: 50 additions & 3 deletions src/features/workspace/components/archive-workspace.tsx
Original file line number Diff line number Diff line change
@@ -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";
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can use the BE error message here, cause it is semantically correct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BE error is Cannot delete default workspace which is actually a remnant, and I was about to open a PR to fix separate to this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right 🤔 I don't know why I remember that it was correct

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyways we can update the BE message too, but I agree on this change for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (
<TooltipTrigger delay={0}>
<TooltipInfoButton aria-label="Contextual help" />
<Tooltip>{text}</Tooltip>
</TooltipTrigger>
);
}

const ButtonsUnarchived = ({ workspaceName }: { workspaceName: string }) => {
const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName });

return <Button {...archiveButtonProps} />;
return (
<div className="flex gap-2 items-center">
<Button {...archiveButtonProps} />
<ContextualHelp workspaceName={workspaceName} />
</div>
);
};

const ButtonsArchived = ({ workspaceName }: { workspaceName: string }) => {
Expand Down Expand Up @@ -51,7 +98,7 @@ export function ArchiveWorkspace({
<CardBody className="flex justify-between items-center">
<div>
<Text className="text-primary">Archive Workspace</Text>
<Text className="flex items-center text-secondary mb-0">
<Text className="flex items-center text-secondary mb-0 text-balance">
Archiving this workspace removes it from the main workspaces list,
though it can be restored if needed.
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export function WorkspaceCustomInstructions({
<CardBody>
<Text className="text-primary">Custom instructions</Text>
<Text className="text-secondary mb-4">
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.
</Text>
<div className="border border-gray-200 rounded overflow-hidden">
{isCustomInstructionsPending ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof Button> {
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export function useConfirmHardDeleteWorkspace() {
async (...params: Parameters<typeof hardDeleteWorkspace>) => {
const answer = await confirm(
<>
<p>Are you sure you want to delete this workspace?</p>
<p className="mb-1">
Are you sure you want to permanently delete this workspace?
</p>
<p>
You will lose any custom instructions, or other configuration.{" "}
<b>This action cannot be undone.</b>
You will lose all configuration and data associated with this
workspace, like prompt history or alerts.
</p>
</>,
{
Expand Down
Loading