Skip to content

Commit e77a837

Browse files
fix: show banner in archived workspace (#165)
* fix: show banner in archived workspace * chore: fix types in tests
1 parent 6fb502c commit e77a837

12 files changed

+123
-36
lines changed

src/components/icons/FlipBackward.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const SvgFlipBackward = (props: SVGProps<SVGSVGElement>) => (
77
{...props}
88
>
99
<path
10-
stroke="#2E323A"
10+
stroke="currentColor"
1111
strokeLinecap="round"
1212
strokeLinejoin="round"
1313
d="M1 5h13.5a4.5 4.5 0 1 1 0 9H10M1 5l4-4M1 5l4 4"

src/features/workspace-system-prompt/components/__tests__/system-prompt-editor.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ vi.mock("@monaco-editor/react", () => {
2121
});
2222

2323
const renderComponent = () =>
24-
render(<SystemPromptEditor workspaceName="foo" />);
24+
render(<SystemPromptEditor isArchived={false} workspaceName="foo" />);
2525

2626
test("can update system prompt", async () => {
2727
server.use(
2828
http.get("*/api/v1/workspaces/:name/system-prompt", () => {
2929
return HttpResponse.json({ prompt: "initial prompt from server" });
30-
}),
30+
})
3131
);
3232

3333
const { getByRole } = renderComponent();
@@ -48,7 +48,7 @@ test("can update system prompt", async () => {
4848
server.use(
4949
http.get("*/api/v1/workspaces/:name/system-prompt", () => {
5050
return HttpResponse.json({ prompt: "new prompt from test" });
51-
}),
51+
})
5252
);
5353

5454
await waitFor(() => {

src/features/workspace-system-prompt/components/system-prompt-editor.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ function usePromptValue({
132132
export function SystemPromptEditor({
133133
className,
134134
workspaceName,
135+
isArchived,
135136
}: {
136137
className?: string;
137138
workspaceName: string;
139+
isArchived: boolean | undefined;
138140
}) {
139141
const context = useContext(DarkModeContext);
140142
const theme: Theme = inferDarkMode(context);
@@ -194,21 +196,22 @@ export function SystemPromptEditor({
194196
<Editor
195197
options={{
196198
minimap: { enabled: false },
199+
readOnly: isArchived,
197200
}}
198201
value={value}
199202
onChange={(v) => setValue(v ?? "")}
200203
height="20rem"
201204
defaultLanguage="Markdown"
202205
theme={theme}
203-
className="bg-base"
206+
className={twMerge("bg-base", isArchived ? "opacity-25" : "")}
204207
/>
205208
)}
206209
</div>
207210
</CardBody>
208211
<CardFooter className="justify-end gap-2">
209212
<Button
210213
isPending={isMutationPending}
211-
isDisabled={Boolean(isGetPromptPending ?? saved)}
214+
isDisabled={Boolean(isArchived ?? isGetPromptPending ?? saved)}
212215
onPress={() => handleSubmit(value)}
213216
>
214217
{saved ? (

src/features/workspace/components/__tests__/archive-workspace.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const mockToast = vi.fn();
88
vi.mock("react-router-dom", async () => {
99
const original =
1010
await vi.importActual<typeof import("react-router-dom")>(
11-
"react-router-dom",
11+
"react-router-dom"
1212
);
1313
return {
1414
...original,
@@ -19,7 +19,7 @@ vi.mock("react-router-dom", async () => {
1919
vi.mock("@stacklok/ui-kit", async () => {
2020
const original =
2121
await vi.importActual<typeof import("@stacklok/ui-kit")>(
22-
"@stacklok/ui-kit",
22+
"@stacklok/ui-kit"
2323
);
2424
return {
2525
...original,
@@ -28,7 +28,7 @@ vi.mock("@stacklok/ui-kit", async () => {
2828
});
2929

3030
test("archive workspace", async () => {
31-
render(<ArchiveWorkspace workspaceName="foo" />);
31+
render(<ArchiveWorkspace isArchived={false} workspaceName="foo" />);
3232

3333
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
3434
await waitFor(() => expect(mockNavigate).toBeCalled());

src/features/workspace/components/archive-workspace.tsx

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { Card, CardBody, Button, Text } from "@stacklok/ui-kit";
22
import { twMerge } from "tailwind-merge";
3-
import { useArchiveWorkspace } from "../../workspace-system-prompt/hooks/use-archive-workspace";
3+
import { useRestoreWorkspaceButton } from "../hooks/use-restore-workspace-button";
4+
import { useArchiveWorkspaceButton } from "../hooks/use-archive-workspace-button";
45

56
export function ArchiveWorkspace({
67
className,
78
workspaceName,
9+
isArchived,
810
}: {
911
workspaceName: string;
1012
className?: string;
13+
isArchived: boolean | undefined;
1114
}) {
12-
const { mutate, isPending } = useArchiveWorkspace();
15+
const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName });
16+
const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName });
1317

1418
return (
1519
<Card className={twMerge(className, "shrink-0")}>
@@ -22,15 +26,7 @@ export function ArchiveWorkspace({
2226
</Text>
2327
</div>
2428

25-
<Button
26-
isDestructive
27-
isPending={isPending}
28-
onPress={() => {
29-
mutate({ path: { workspace_name: workspaceName } });
30-
}}
31-
>
32-
Archive
33-
</Button>
29+
<Button {...(isArchived ? restoreButtonProps : archiveButtonProps)} />
3430
</CardBody>
3531
</Card>
3632
);

src/features/workspace/components/workspace-name.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import { FormEvent, useState } from "react";
1515
export function WorkspaceName({
1616
className,
1717
workspaceName,
18+
isArchived,
1819
}: {
1920
className?: string;
2021
workspaceName: string;
22+
isArchived: boolean | undefined;
2123
}) {
2224
const [name, setName] = useState(workspaceName);
2325
const { mutate, isPending, error } = useCreateWorkspace();
@@ -38,6 +40,7 @@ export function WorkspaceName({
3840
name="Workspace name"
3941
validationBehavior="aria"
4042
isRequired
43+
isDisabled={isArchived}
4144
onChange={setName}
4245
>
4346
<Label>Workspace name</Label>
@@ -46,7 +49,11 @@ export function WorkspaceName({
4649
</TextField>
4750
</CardBody>
4851
<CardFooter className="justify-end gap-2">
49-
<Button isDisabled={name === ""} isPending={isPending} type="submit">
52+
<Button
53+
isDisabled={isArchived || name === ""}
54+
isPending={isPending}
55+
type="submit"
56+
>
5057
Save
5158
</Button>
5259
</CardFooter>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Button } from "@stacklok/ui-kit";
2+
import { ComponentProps } from "react";
3+
import { useArchiveWorkspace } from "@/features/workspace-system-prompt/hooks/use-archive-workspace";
4+
5+
export function useArchiveWorkspaceButton({
6+
workspaceName,
7+
}: {
8+
workspaceName: string;
9+
}): ComponentProps<typeof Button> {
10+
const { mutate, isPending } = useArchiveWorkspace();
11+
12+
return {
13+
isPending,
14+
isDisabled: isPending,
15+
onPress: () => mutate({ path: { workspace_name: workspaceName } }),
16+
isDestructive: true,
17+
children: "Archive",
18+
};
19+
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { v1ListArchivedWorkspacesOptions } from "@/api/generated/@tanstack/react-query.gen";
3+
import { V1ListArchivedWorkspacesResponse } from "@/api/generated";
34

4-
export const useArchivedWorkspaces = () => {
5+
export function useArchivedWorkspaces<T = V1ListArchivedWorkspacesResponse>({
6+
select,
7+
}: {
8+
select?: (data: V1ListArchivedWorkspacesResponse) => T;
9+
} = {}) {
510
return useQuery({
611
...v1ListArchivedWorkspacesOptions(),
7-
refetchInterval: 5_000,
12+
refetchInterval: 5000,
813
refetchIntervalInBackground: true,
914
refetchOnMount: true,
1015
refetchOnReconnect: true,
1116
refetchOnWindowFocus: true,
1217
retry: false,
18+
select,
1319
});
14-
};
20+
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { v1ListWorkspacesOptions } from "@/api/generated/@tanstack/react-query.gen";
3+
import { V1ListWorkspacesResponse } from "@/api/generated";
34

4-
export const useListWorkspaces = () => {
5+
export function useListWorkspaces<T = V1ListWorkspacesResponse>({
6+
select,
7+
}: {
8+
select?: (data: V1ListWorkspacesResponse) => T;
9+
} = {}) {
510
return useQuery({
611
...v1ListWorkspacesOptions(),
7-
refetchInterval: 5_000,
12+
refetchInterval: 5000,
813
refetchIntervalInBackground: true,
914
refetchOnMount: true,
1015
refetchOnReconnect: true,
1116
refetchOnWindowFocus: true,
1217
retry: false,
18+
select,
1319
});
14-
};
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Button } from "@stacklok/ui-kit";
2+
import { ComponentProps } from "react";
3+
import { useRestoreWorkspace } from "./use-restore-workspace";
4+
5+
export function useRestoreWorkspaceButton({
6+
workspaceName,
7+
}: {
8+
workspaceName: string;
9+
}): ComponentProps<typeof Button> {
10+
const { mutate, isPending } = useRestoreWorkspace();
11+
12+
return {
13+
isPending,
14+
isDisabled: isPending,
15+
onPress: () => mutate({ path: { workspace_name: workspaceName } }),
16+
children: "Restore",
17+
};
18+
}

src/routes/route-workspace.tsx

+38-4
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,37 @@ import { ArchiveWorkspace } from "@/features/workspace/components/archive-worksp
33
import { SystemPromptEditor } from "@/features/workspace-system-prompt/components/system-prompt-editor";
44
import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading";
55
import { WorkspaceName } from "@/features/workspace/components/workspace-name";
6-
import { Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit";
6+
import { Alert, Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit";
77
import { useParams } from "react-router-dom";
8+
import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-workspaces";
9+
import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button";
10+
11+
function WorkspaceArchivedBanner({ name }: { name: string }) {
12+
const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name });
13+
14+
return (
15+
<Alert
16+
variant="warning"
17+
title="This workspace has been archived"
18+
className="mb-8 animate-in fade-in zoom-in-95"
19+
actionButtonProps={restoreButtonProps}
20+
>
21+
You can still view this workspace's configuration. To begin using it
22+
again, you must restore it.
23+
</Alert>
24+
);
25+
}
826

927
export function RouteWorkspace() {
1028
const { name } = useParams();
1129

1230
if (!name) throw Error("Workspace name is required");
1331

32+
const { data: isArchived } = useArchivedWorkspaces<boolean>({
33+
select: (data) =>
34+
data?.workspaces.find((w) => w.name === name) !== undefined,
35+
});
36+
1437
return (
1538
<>
1639
<Breadcrumbs>
@@ -20,9 +43,20 @@ export function RouteWorkspace() {
2043
</Breadcrumbs>
2144

2245
<WorkspaceHeading title="Workspace settings" />
23-
<WorkspaceName className="mb-4" workspaceName={name} />
24-
<SystemPromptEditor workspaceName={name} className="mb-4" />
25-
<ArchiveWorkspace workspaceName={name} />
46+
47+
{isArchived ? <WorkspaceArchivedBanner name={name} /> : null}
48+
49+
<WorkspaceName
50+
isArchived={isArchived}
51+
className="mb-4"
52+
workspaceName={name}
53+
/>
54+
<SystemPromptEditor
55+
isArchived={isArchived}
56+
workspaceName={name}
57+
className="mb-4"
58+
/>
59+
<ArchiveWorkspace isArchived={isArchived} workspaceName={name} />
2660
</>
2761
);
2862
}

src/routes/route-workspaces.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Settings, SquarePlus } from "lucide-react";
1818
import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-workspaces";
1919
import { Workspace } from "@/api/generated";
2020
import SvgFlipBackward from "@/components/icons/FlipBackward";
21-
import { useRestoreWorkspace } from "@/features/workspace/hooks/use-restore-workspace";
21+
import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button";
2222

2323
function CellName({
2424
name,
@@ -48,17 +48,15 @@ function CellConfiguration({
4848
name: string;
4949
isArchived?: boolean;
5050
}) {
51-
const { mutate, isPending } = useRestoreWorkspace();
51+
const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name });
5252

5353
if (isArchived) {
5454
return (
5555
<Cell>
5656
<Button
5757
variant="tertiary"
58-
isPending={isPending}
59-
isDisabled={isPending}
6058
className="flex w-full gap-2 items-center"
61-
onPress={() => mutate({ path: { workspace_name: name } })}
59+
{...restoreButtonProps}
6260
>
6361
<SvgFlipBackward /> Restore Configuration
6462
</Button>

0 commit comments

Comments
 (0)