chore(site): migrate all <Dialog />s off MUI (#27506)

> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Removes Material UI from every dialog, moving them onto the internal
shadcn/radix `Dialog` primitives. After this change there are **no**
`@mui/material/Dialog` usages left in `site/src`.

## Changes

- Consolidated `components/Dialogs/*` → `components/Dialog/*` and folded
the old `ConfirmDeleteDialog` into `ConfirmDialog` (`type="delete"`).
- Rewrote `ConfirmDialog`, `DeleteDialog`, `WorkspaceDeleteDialog`,
`ScheduleDialog`, and `AnnouncementBannerDialog` onto the internal
primitives (native `Input`/`Label`/`Checkbox`/`Link` instead of MUI).
- Finished the migration for the last two MUI hold-outs:
`UpdateBuildParametersDialog` and `MissingTemplateVariablesDialog`.
- Dialog prop types now compose the rendered component's props
(`ComponentProps<typeof Dialog>` / MUI `DialogProps`) instead of
hand-rolled `{ open; onOpenChange }` shapes.

## Testing

AI-Driven manual dogfood sweep in a live instance (premium license),
driven in a browser. Each dialog checked for logical rendering (layout,
variant styling, buttons, no overlap/blank/console error) and function
(open, primary action, cancel/close, guard states):

| Dialog / surface | How tested | Result |
| --- | --- | --- |
| `ConfirmDialog` (delete / info / success) | Token delete,
update-confirm, change-version |  |
| `DeleteDialog` (type-to-confirm) | Delete user, group, license,
provider, OAuth2 app |  |
| `WorkspaceDeleteDialog` | Workspace actions → Delete (+ orphan path
via failed workspace) |  |
| `ScheduleDialog` | Template schedule → dormancy/deletion warning |  |
| `AnnouncementBannerDialog` | Deployment → Appearance → New banner
(live preview + color) |  |
| `ChangeWorkspaceVersionDialog` | Workspace actions → Change version |
 |
| `DownloadLogsDialog` | Workspace actions → Download logs |  |
| Batch delete (workspaces) | Workspaces list → multi-select → Delete |
 |
| `TemplatePageHeader` delete | Template → Delete (cancelled) |  |
| `FileDialog` (create/rename/delete) | Template editor file tree |  |
| `MissingTemplateVariablesDialog` *(migrated)* | Editor → add variable
→ Build |  |
| `PublishTemplateVersionDialog` | Editor → Publish |  |
| `UpdateBuildParametersDialog` *(migrated)* | Classic flow + required
param → workspace Update (renders + submits) |  |
| Update-confirmation (`WorkspaceUpdateDialogs`) | Workspace Update | 
|
| Suspend/activate confirm | Users → member row |  |
| `ResetPasswordDialog` | Users → member → Reset password |  |
| Token delete confirm | Settings → Tokens |  |
| Create-token confirm | Token create flow |  |
| SSH key regenerate confirm | Settings → SSH Keys |  |
| Change-login-type confirm | Settings → Security |  |
| Secret delete | Settings → Secrets |  |
| Group delete | Admin → Groups |  |
| Org member remove | Admin → Organization → Members |  |
| License remove | Deployment → Licenses (cancelled) |  |
| Announcement banner delete | Deployment → Appearance |  |
| OAuth2 app delete | Deployment → OAuth2 apps |  |
| `ModelFormDialogs` (form + delete) | AI → Models |  |
| Provider delete | AI → Providers |  |
| Gateway key create/delete | AI → Gateway keys |  |
| `MCPServerFormDialogs` delete | AI → MCP servers |  |
| Personal skill (create form + delete) | Agents → Personal Skills |  |
| Spend user-override (add + delete) | AI → Spend |  |

Human tested:

- [x] template version promote/archive
- [x] external-auth/OAuth2-provider delete 
- [x] custom-role delete
- [x] cancel-provisioner-job

Things that have a chance to bleed:

- tasks dialogs
- dormant inline confirm

Should be known that each of these renders through the already-verified
`ConfirmDialog`/`DeleteDialog`, so the underlying component is covered
even where the specific trigger wasn't reachable.

<details>
<summary>Plan &amp; decision log</summary>

**Goal:** finish the de-MUI migration everywhere and confirm every
affected modal renders logically and functions.

**Phase 1 - complete the migration**

- Confirmed only two files still rendered MUI `Dialog`
(`UpdateBuildParametersDialog`, `MissingTemplateVariablesDialog`);
ported both to the internal primitives, preserving the `{ open, onClose,
... }` public API (mapped to `onOpenChange` internally) so call sites
were unchanged. radix now wires `aria-labelledby`/`aria-describedby`,
removing a duplicated element id.

**Phase 2 - sighting sweep (live browser, premium license)**

- Batch A (workspaces/tasks): 4 PASS, rest state-gated.
- Batch B (templates): `MissingTemplateVariablesDialog`, `FileDialog`,
`PublishTemplateVersionDialog`, template delete - all PASS.
- `UpdateBuildParametersDialog`: reached by enabling classic parameter
flow + pushing a version with a required parameter - PASS (renders +
submits).
- Batch C (users/org/settings): 10 PASS.
- Batch D (deployment/AI): 10 PASS.

**Decisions**

- Kept `ConfirmDialog`-wrapper prop types explicit (composing
`DialogProps` there reintroduced a MUI smell).
- Dropped the unused `ConfirmDialogType` export (knip) and updated the
`WorkspacePage` orphan-delete test: the radix `Checkbox` puts the test
id on the `role=checkbox` button itself, so the previous
`within(...).getByRole` no longer matched.

</details>
This commit is contained in:
Jake Howell
2026-07-31 03:23:00 +00:00
committed by GitHub
parent bc9c7855d9
commit 79724ab0ba
74 changed files with 1001 additions and 1069 deletions
@@ -0,0 +1,100 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, screen, userEvent } from "storybook/test";
import { ConfirmDialog } from "./ConfirmDialog";
const meta: Meta<typeof ConfirmDialog> = {
title: "components/Dialog/ConfirmDialog",
component: ConfirmDialog,
args: {
onClose: fn(),
onConfirm: fn(),
open: true,
title: "Confirm Dialog",
},
};
export default meta;
type Story = StoryObj<typeof ConfirmDialog>;
export const Example: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
},
play: async ({ args }) => {
const dialog = await screen.findByRole("dialog");
await expect(dialog).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
await expect(args.onClose).toHaveBeenCalled();
},
};
export const InfoDialog: Story = {
args: {
description: "Information is cool!",
hideCancel: true,
type: "info",
},
play: async ({ args }) => {
await expect(
screen.queryByRole("button", { name: "Cancel" }),
).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "OK" }));
await expect(args.onConfirm).toHaveBeenCalled();
},
};
export const InfoDialogWithCancel: Story = {
args: {
description: "Information can be cool!",
hideCancel: false,
type: "info",
},
};
export const SuccessDialog: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
},
};
export const SuccessDialogWithCancel: Story = {
args: {
description: "I may be successful.",
hideCancel: false,
type: "success",
},
};
export const SuccessDialogLoading: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
confirmLoading: true,
},
play: async () => {
// Spinner prefixes the accessible name with "Loading spinner".
await expect(screen.getByRole("button", { name: /ok/i })).toBeDisabled();
},
};
export const ConfirmAction: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
confirmText: "CONFIRM",
cancelText: "CANCEL",
},
play: async ({ args }) => {
await userEvent.click(screen.getByRole("button", { name: "CONFIRM" }));
await expect(args.onConfirm).toHaveBeenCalled();
await expect(args.onClose).not.toHaveBeenCalled();
},
};
@@ -0,0 +1,115 @@
import type { FC, ReactNode } from "react";
import {
Dialog,
DialogActions,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
type ConfirmDialogType = "delete" | "info" | "success";
interface ConfirmDialogTypeConfig {
confirmText: ReactNode;
hideCancel: boolean;
}
const CONFIRM_DIALOG_DEFAULTS: Record<
ConfirmDialogType,
ConfirmDialogTypeConfig
> = {
delete: {
confirmText: "Delete",
hideCancel: false,
},
info: {
confirmText: "OK",
hideCancel: true,
},
success: {
confirmText: "OK",
hideCancel: true,
},
};
export interface ConfirmDialogProps {
readonly title: string;
readonly open: boolean;
readonly onClose: () => void;
readonly description: ReactNode;
readonly cancelText?: string;
readonly confirmText?: ReactNode;
readonly confirmLoading?: boolean;
readonly disabled?: boolean;
/**
* When omitted, `onClose` doubles as the confirm handler, so a `delete`
* dialog without `onConfirm` closes without deleting anything.
*/
readonly onConfirm?: () => void;
readonly type?: ConfirmDialogType;
/**
* Defaults to shown for "delete", hidden for "info"/"success".
*/
readonly hideCancel?: boolean;
}
/**
* Quick-use dialog for yes/no style confirmations without custom layout.
*/
export const ConfirmDialog: FC<ConfirmDialogProps> = ({
cancelText = "Cancel",
confirmLoading = false,
confirmText,
description,
disabled = false,
hideCancel,
onClose,
onConfirm,
open = false,
title,
type = "info",
}) => {
const defaults = CONFIRM_DIALOG_DEFAULTS[type];
const shouldHideCancel = hideCancel ?? defaults.hideCancel;
const resolvedConfirmText = confirmText ?? defaults.confirmText;
const handleConfirm = onConfirm ?? onClose;
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
>
<DialogContent
variant={type === "delete" ? "destructive" : "default"}
data-testid="dialog"
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription asChild>
<div className="text-sm text-content-secondary font-medium [&_strong]:text-content-primary [&_p]:m-0 [&_p+p]:mt-2">
{description}
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogActions
cancelText={cancelText}
onCancel={shouldHideCancel ? undefined : onClose}
confirmText={resolvedConfirmText}
confirmLoading={confirmLoading}
confirmDisabled={disabled}
confirmVariant={type === "delete" ? "destructive" : undefined}
onConfirm={handleConfirm}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { DeleteDialog } from "./DeleteDialog";
const meta: Meta<typeof DeleteDialog> = {
title: "components/Dialog/DeleteDialog",
component: DeleteDialog,
args: {
onCancel: fn(),
onConfirm: fn(),
isOpen: true,
entity: "foo",
name: "MyFoo",
info: "Here's some info about the foo so you know you're deleting the right one.",
},
};
export default meta;
type Story = StoryObj<typeof DeleteDialog>;
export const Idle: Story = {
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
},
};
export const FilledSuccessfully: Story = {
play: async ({ canvasElement, args }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "MyFoo");
const confirmButton = body.getByRole("button", { name: "Delete" });
await expect(confirmButton).toBeEnabled();
await user.click(confirmButton);
await expect(args.onConfirm).toHaveBeenCalled();
},
};
export const FilledWrong: Story = {
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "InvalidFooName");
// Blur so the mismatch error becomes visible.
await user.tab();
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
await expect(
body.getByText("InvalidFooName does not match the name of this foo"),
).toBeVisible();
},
};
export const Loading: Story = {
args: {
confirmLoading: true,
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
// Spinner prefixes the accessible name with "Loading spinner".
await expect(body.getByRole("button", { name: /delete/i })).toBeDisabled();
await expect(body.getByRole("button", { name: "Cancel" })).toBeDisabled();
},
};
@@ -0,0 +1,144 @@
import { type FC, type FormEvent, useId, useState } from "react";
import { Alert } from "#/components/Alert/Alert";
import { Button } from "#/components/Button/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { Input } from "#/components/Input/Input";
import { Label } from "#/components/Label/Label";
import { Spinner } from "#/components/Spinner/Spinner";
interface DeleteDialogProps {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
entity: string;
name: string;
info?: string;
confirmLoading?: boolean;
verb?: string;
title?: string;
label?: string;
confirmText?: string;
}
export const DeleteDialog: FC<DeleteDialogProps> = ({
isOpen,
onCancel,
onConfirm,
entity,
info,
name,
confirmLoading = false,
// Optional overrides for verbiage, e.g. "unlinking" vs "deleting".
verb,
title,
label,
confirmText = "Delete",
}) => {
const confirmId = useId();
const errorId = `${confirmId}-error`;
const [userConfirmationText, setUserConfirmationText] = useState("");
const [isFocused, setIsFocused] = useState(false);
const deletionConfirmed = name === userConfirmationText;
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
const displayErrorMessage = hasError && !isFocused;
const resetConfirmation = () => {
setUserConfirmationText("");
setIsFocused(false);
};
const handleOpenChange = (open: boolean) => {
if (!open) {
resetConfirmation();
onCancel();
}
};
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (deletionConfirmed && !confirmLoading) {
onConfirm();
}
};
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent variant="destructive" data-testid="dialog">
<DialogHeader>
<DialogTitle>{title ?? `Delete ${entity}`}</DialogTitle>
<DialogDescription>
{verb ?? "Deleting"} this {entity} is irreversible!
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
{info && (
<Alert severity="warning" prominent>
{info}
</Alert>
)}
<p className="m-0 text-sm text-content-secondary font-medium">
Type <strong className="text-content-primary">{name}</strong> below
to confirm.
</p>
</div>
<form className="flex flex-col gap-6" onSubmit={onSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor={confirmId}>
{label ?? `Name of the ${entity} to delete`}
</Label>
<Input
id={confirmId}
name="confirmation"
autoComplete="off"
autoFocus
placeholder={name}
value={userConfirmationText}
onChange={(event) => setUserConfirmationText(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
aria-invalid={displayErrorMessage}
aria-describedby={displayErrorMessage ? errorId : undefined}
data-testid="delete-dialog-name-confirmation"
/>
{displayErrorMessage && (
<span id={errorId} className="text-xs text-content-destructive">
{userConfirmationText} does not match the name of this {entity}
</span>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={confirmLoading}
onClick={() => handleOpenChange(false)}
>
Cancel
</Button>
<Button
type="submit"
variant="destructive"
disabled={!deletionConfirmed || confirmLoading}
data-testid="confirm-button"
>
<Spinner loading={confirmLoading} />
{confirmText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
+1 -1
View File
@@ -33,7 +33,7 @@ const DialogOverlay: React.FC<
};
const dialogVariants = cva(
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
`fixed left-[50%] top-[50%] z-50 grid max-h-[90vh] w-full max-w-lg gap-6 overflow-y-auto
border border-solid bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
translate-x-[-50%] translate-y-[-50%] outline-none
data-[state=open]:animate-in data-[state=closed]:animate-out
@@ -1,62 +0,0 @@
import type { FC, ReactNode } from "react";
import { Button } from "#/components/Button/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { Spinner } from "#/components/Spinner/Spinner";
interface ConfirmDeleteDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** The entity type being deleted, shown in the title and button. */
entity: string;
/**
* Optional description. Defaults to "Are you sure you want to
* delete this {entity}? This action is irreversible."
*/
description?: ReactNode;
children?: ReactNode;
onConfirm: () => void;
isPending?: boolean;
}
export const ConfirmDeleteDialog: FC<ConfirmDeleteDialogProps> = ({
open,
onOpenChange,
entity,
description,
children,
onConfirm,
isPending = false,
}) => (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent variant="destructive">
<DialogHeader>
<DialogTitle>Delete {entity}</DialogTitle>
<DialogDescription>
{description ??
`Are you sure you want to delete this ${entity}? This action is irreversible.`}
</DialogDescription>
</DialogHeader>
{children}
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
Cancel
</Button>
<Button variant="destructive" onClick={onConfirm} disabled={isPending}>
{isPending && <Spinner className="size-4" loading />}
Delete {entity}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
@@ -1,66 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { ConfirmDialog } from "./ConfirmDialog";
const meta: Meta<typeof ConfirmDialog> = {
title: "components/Dialogs/ConfirmDialog",
component: ConfirmDialog,
args: {
onClose: action("onClose"),
onConfirm: action("onConfirm"),
open: true,
title: "Confirm Dialog",
},
};
export default meta;
type Story = StoryObj<typeof ConfirmDialog>;
export const Example: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
},
};
export const InfoDialog: Story = {
args: {
description: "Information is cool!",
hideCancel: true,
type: "info",
},
};
export const InfoDialogWithCancel: Story = {
args: {
description: "Information can be cool!",
hideCancel: false,
type: "info",
},
};
export const SuccessDialog: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
},
};
export const SuccessDialogWithCancel: Story = {
args: {
description: "I may be successful.",
hideCancel: false,
type: "success",
},
};
export const SuccessDialogLoading: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
confirmLoading: true,
},
};
@@ -1,47 +0,0 @@
import { fireEvent, screen } from "@testing-library/react";
import { renderComponent } from "#/testHelpers/renderHelpers";
import { ConfirmDialog } from "./ConfirmDialog";
describe("ConfirmDialog", () => {
it("onClose is called when cancelled", () => {
// Given
const onCloseMock = vi.fn();
const props = {
cancelText: "CANCEL",
hideCancel: false,
onClose: onCloseMock,
open: true,
title: "Test",
};
// When
renderComponent(<ConfirmDialog {...props} />);
fireEvent.click(screen.getByText("CANCEL"));
// Then
expect(onCloseMock).toBeCalledTimes(1);
});
it("onConfirm is called when confirmed", () => {
// Given
const onCloseMock = vi.fn();
const onConfirmMock = vi.fn();
const props = {
cancelText: "CANCEL",
confirmText: "CONFIRM",
hideCancel: false,
onClose: onCloseMock,
onConfirm: onConfirmMock,
open: true,
title: "Test",
};
// When
renderComponent(<ConfirmDialog {...props} />);
fireEvent.click(screen.getByText("CONFIRM"));
// Then
expect(onCloseMock).toBeCalledTimes(0);
expect(onConfirmMock).toBeCalledTimes(1);
});
});
@@ -1,145 +0,0 @@
import type { Interpolation, Theme } from "@emotion/react";
import DialogActions from "@mui/material/DialogActions";
import type { FC, ReactNode } from "react";
import {
Dialog,
DialogActionButtons,
type DialogActionButtonsProps,
} from "../Dialog";
import type { ConfirmDialogType } from "../types";
interface ConfirmDialogTypeConfig {
confirmText: ReactNode;
hideCancel: boolean;
}
const CONFIRM_DIALOG_DEFAULTS: Record<
ConfirmDialogType,
ConfirmDialogTypeConfig
> = {
delete: {
confirmText: "Delete",
hideCancel: false,
},
info: {
confirmText: "OK",
hideCancel: true,
},
success: {
confirmText: "OK",
hideCancel: true,
},
};
export interface ConfirmDialogProps
extends Omit<DialogActionButtonsProps, "color" | "onCancel"> {
readonly description?: ReactNode;
/**
* hideCancel hides the cancel button when set true, and shows the cancel
* button when set to false. When undefined:
* - cancel is not displayed for "info" dialogs
* - cancel is displayed for "delete" dialogs
*/
readonly hideCancel?: boolean;
/**
* onClose is called when canceling (if cancel is showing).
*
* Additionally, if onConfirm is not defined onClose will be used in its place
* when confirming.
*/
readonly onClose: () => void;
readonly open: boolean;
readonly title: string;
}
const styles = {
dialogWrapper: (theme) => ({
"& .MuiPaper-root": {
background: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
width: "100%",
maxWidth: 440,
},
"& .MuiDialogActions-spacing": {
padding: "0 40px 40px",
},
}),
dialogContent: (theme) => ({
color: theme.palette.text.secondary,
padding: "40px 40px 20px",
}),
dialogTitle: (theme) => ({
margin: 0,
marginBottom: 16,
color: theme.palette.text.primary,
fontWeight: 400,
fontSize: 20,
}),
dialogDescription: (theme) => ({
color: theme.palette.text.secondary,
lineHeight: "160%",
fontSize: 16,
"& strong": {
color: theme.palette.text.primary,
},
"& p:not(.MuiFormHelperText-root)": {
margin: 0,
},
"& > p": {
margin: "8px 0",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
/**
* Quick-use version of the Dialog component with slightly alternative styles,
* great to use for dialogs that don't have any interaction beyond yes / no.
*/
export const ConfirmDialog: FC<ConfirmDialogProps> = ({
cancelText,
confirmLoading,
confirmText,
description,
disabled = false,
hideCancel,
onClose,
onConfirm,
open = false,
title,
type = "info",
}) => {
const defaults = CONFIRM_DIALOG_DEFAULTS[type];
if (typeof hideCancel === "undefined") {
hideCancel = defaults.hideCancel;
}
return (
<Dialog
css={styles.dialogWrapper}
onClose={onClose}
open={open}
data-testid="dialog"
>
<div css={styles.dialogContent}>
<h3 css={styles.dialogTitle}>{title}</h3>
{description && <div css={styles.dialogDescription}>{description}</div>}
</div>
<DialogActions>
<DialogActionButtons
cancelText={cancelText}
confirmLoading={confirmLoading}
confirmText={confirmText || defaults.confirmText}
disabled={disabled}
onCancel={!hideCancel ? onClose : undefined}
onConfirm={onConfirm || onClose}
type={type}
/>
</DialogActions>
</Dialog>
);
};
@@ -1,47 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { userEvent, within } from "storybook/test";
import { DeleteDialog } from "./DeleteDialog";
const meta: Meta<typeof DeleteDialog> = {
title: "components/Dialogs/DeleteDialog",
component: DeleteDialog,
args: {
onCancel: action("onClose"),
onConfirm: action("onConfirm"),
isOpen: true,
entity: "foo",
name: "MyFoo",
info: "Here's some info about the foo so you know you're deleting the right one.",
},
};
export default meta;
type Story = StoryObj<typeof DeleteDialog>;
export const Idle: Story = {};
export const FilledSuccessfully: Story = {
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "MyFoo");
},
};
export const FilledWrong: Story = {
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "InvalidFooName");
},
};
export const Loading: Story = {
args: {
confirmLoading: true,
},
};
@@ -1,72 +0,0 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react";
import { renderComponent } from "#/testHelpers/renderHelpers";
import { DeleteDialog } from "./DeleteDialog";
const inputTestId = "delete-dialog-name-confirmation";
async function fillInputField(inputElement: HTMLElement, text: string) {
// 2023-10-06 - There's something wonky with MUI's ConfirmDialog that causes
// its state to update after a typing event gets fired, and React Testing
// Library isn't able to catch it, making React DOM freak out because an
// "unexpected" state change happened. It won't fail the test, but it makes
// the console look really scary because it'll spit out a big warning message.
// Tried everything under the sun to catch the state changes the proper way,
// but the only way to get around it for now might be to manually make React
// DOM aware of the changes
return act(() => userEvent.type(inputElement, text));
}
describe("DeleteDialog", () => {
it("disables confirm button when the text field is empty", () => {
renderComponent(
<DeleteDialog
isOpen
onConfirm={vi.fn()}
onCancel={vi.fn()}
entity="template"
name="MyTemplate"
/>,
);
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
});
it("disables confirm button when the text field is filled incorrectly", async () => {
renderComponent(
<DeleteDialog
isOpen
onConfirm={vi.fn()}
onCancel={vi.fn()}
entity="template"
name="MyTemplate"
/>,
);
const textField = screen.getByTestId(inputTestId);
await fillInputField(textField, "MyTemplateButWrong");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
});
it("enables confirm button when the text field is filled correctly", async () => {
renderComponent(
<DeleteDialog
isOpen
onConfirm={vi.fn()}
onCancel={vi.fn()}
entity="template"
name="MyTemplate"
/>,
);
const textField = screen.getByTestId(inputTestId);
await fillInputField(textField, "MyTemplate");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).not.toBeDisabled();
});
});
@@ -1,108 +0,0 @@
import TextField from "@mui/material/TextField";
import { useId, useState } from "react";
import { Alert } from "#/components/Alert/Alert";
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
interface DeleteDialogProps {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
entity: string;
name: string;
info?: string;
confirmLoading?: boolean;
verb?: string;
title?: string;
label?: string;
confirmText?: string;
}
export const DeleteDialog: React.FC<DeleteDialogProps> = ({
isOpen,
onCancel,
onConfirm,
entity,
info,
name,
confirmLoading,
// All optional to change the verbiage. For example, "unlinking" vs "deleting"
verb,
title,
label,
confirmText,
}) => {
const hookId = useId();
const [userConfirmationText, setUserConfirmationText] = useState("");
const [isFocused, setIsFocused] = useState(false);
const deletionConfirmed = name === userConfirmationText;
const onSubmit = (event: React.SubmitEvent) => {
event.preventDefault();
if (deletionConfirmed) {
onConfirm();
}
};
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
const displayErrorMessage = hasError && !isFocused;
const inputColor = hasError ? "error" : "primary";
return (
<ConfirmDialog
type="delete"
hideCancel={false}
open={isOpen}
title={title ?? `Delete ${entity}`}
onConfirm={onConfirm}
onClose={onCancel}
confirmLoading={confirmLoading}
disabled={!deletionConfirmed}
confirmText={confirmText}
description={
<>
<div className="flex flex-col gap-3">
<p>
{verb ?? "Deleting"} this {entity} is irreversible!
</p>
{Boolean(info) && (
<Alert severity="warning" prominent>
{info}
</Alert>
)}
<p>
Type <strong>{name}</strong> below to confirm.
</p>
</div>
<form onSubmit={onSubmit}>
<TextField
fullWidth
autoFocus
className="mt-6"
name="confirmation"
autoComplete="off"
id={`${hookId}-confirm`}
placeholder={name}
value={userConfirmationText}
onChange={(event) => setUserConfirmationText(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
label={label ?? `Name of the ${entity} to delete`}
color={inputColor}
error={displayErrorMessage}
helperText={
displayErrorMessage &&
`${userConfirmationText} does not match the name of this ${entity}`
}
InputProps={{ color: inputColor }}
inputProps={{
"data-testid": "delete-dialog-name-confirmation",
}}
/>
</form>
</>
}
/>
);
};
-70
View File
@@ -1,70 +0,0 @@
import MuiDialog, { type DialogProps } from "@mui/material/Dialog";
import type { FC, ReactNode } from "react";
import { Button } from "#/components/Button/Button";
import { Spinner } from "#/components/Spinner/Spinner";
import type { ConfirmDialogType } from "./types";
export interface DialogActionButtonsProps {
/** Text to display in the cancel button */
cancelText?: string;
/** Text to display in the confirm button */
confirmText?: ReactNode;
/** Whether or not confirm is loading, also disables cancel when true */
confirmLoading?: boolean;
/** Whether or not the submit button is disabled */
disabled?: boolean;
/** Called when cancel is clicked */
onCancel?: () => void;
/** Called when confirm is clicked */
onConfirm?: () => void;
type?: ConfirmDialogType;
}
/**
* Quickly handles most modals actions, some combination of a cancel and confirm button
*/
export const DialogActionButtons: FC<DialogActionButtonsProps> = ({
cancelText = "Cancel",
confirmText = "Confirm",
confirmLoading = false,
disabled = false,
onCancel,
onConfirm,
type = "info",
}) => {
return (
<>
{onCancel && (
<Button
disabled={confirmLoading}
onClick={(e) => {
e.stopPropagation();
onCancel();
}}
variant="outline"
>
{cancelText}
</Button>
)}
{onConfirm && (
<Button
variant={type === "delete" ? "destructive" : undefined}
disabled={confirmLoading || disabled}
onClick={onConfirm}
data-testid="confirm-button"
type="submit"
>
<Spinner loading={confirmLoading} />
{confirmText}
</Button>
)}
</>
);
};
/**
* Re-export of MUI's Dialog component, for convenience.
* @link See original documentation here: https://mui.com/material-ui/react-dialog/
*/
export { type DialogProps, MuiDialog as Dialog };
-1
View File
@@ -1 +0,0 @@
export type ConfirmDialogType = "delete" | "info" | "success";
@@ -1,7 +1,7 @@
import { EllipsisVerticalIcon } from "lucide-react";
import { type FC, useId, useState } from "react";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -4,7 +4,7 @@ import { toast } from "sonner";
import { API } from "#/api/api";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import type { Task } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
type TaskDeleteDialogProps = {
open: boolean;
@@ -1,6 +1,6 @@
import { useFormik } from "formik";
import { FrownIcon, MehIcon, SmileIcon } from "lucide-react";
import type { FC, HTMLProps, ReactNode } from "react";
import type { ComponentProps, FC, HTMLProps, ReactNode } from "react";
import { useMutation } from "react-query";
import { toast } from "sonner";
import {
@@ -19,7 +19,6 @@ import {
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import type { DialogProps } from "#/components/Dialogs/Dialog";
import { Spinner } from "#/components/Spinner/Spinner";
import { Textarea } from "#/components/Textarea/Textarea";
@@ -28,7 +27,7 @@ type TaskFeedbackFormValues = {
comment: string;
};
type TaskFeedbackDialogProps = DialogProps & {
type TaskFeedbackDialogProps = ComponentProps<typeof Dialog> & {
taskId: string;
};
@@ -1,6 +1,6 @@
import type { FC } from "react";
import type { Workspace } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
interface WorkspaceBuildCancelDialogProps {
open: boolean;
@@ -16,8 +16,7 @@ import {
ComboboxList,
ComboboxTrigger,
} from "#/components/Combobox/Combobox";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import type { DialogProps } from "#/components/Dialogs/Dialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import type { SelectFilterOption } from "#/components/Filter/SelectFilter";
import { FormFields } from "#/components/Form/Form";
import { Loader } from "#/components/Loader/Loader";
@@ -26,7 +25,8 @@ import { TemplateUpdateMessage } from "#/modules/templates/TemplateUpdateMessage
import { cn } from "#/utils/cn";
import { createDayString } from "#/utils/createDayString";
type ChangeWorkspaceVersionDialogProps = DialogProps & {
type ChangeWorkspaceVersionDialogProps = {
open: boolean;
workspace: Workspace;
onClose: () => void;
onConfirm: (version: TemplateVersion) => void;
@@ -34,7 +34,7 @@ type ChangeWorkspaceVersionDialogProps = DialogProps & {
export const ChangeWorkspaceVersionDialog: FC<
ChangeWorkspaceVersionDialogProps
> = ({ workspace, onClose, onConfirm, ...dialogProps }) => {
> = ({ workspace, onClose, onConfirm, open }) => {
const { data: versions } = useQuery({
...templateVersions(workspace.template_id),
select: (data) => [...data].reverse(),
@@ -65,7 +65,7 @@ export const ChangeWorkspaceVersionDialog: FC<
return (
<ConfirmDialog
{...dialogProps}
open={open}
onClose={onClose}
onConfirm={() => {
if (newVersion) {
@@ -10,7 +10,7 @@ import { Alert } from "#/components/Alert/Alert";
import {
ConfirmDialog,
type ConfirmDialogProps,
} from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
} from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { cn } from "#/utils/cn";
import { getWorkspaceAgents } from "#/utils/workspace";
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import {
MockFailedWorkspace,
MockTaskWorkspace,
@@ -20,13 +21,27 @@ const meta: Meta<typeof WorkspaceDeleteDialog> = {
},
canDeleteFailedWorkspace: false,
isOpen: true,
onCancel: fn(),
onConfirm: fn(),
},
};
export default meta;
type Story = StoryObj<typeof WorkspaceDeleteDialog>;
export const Example: Story = {};
export const Example: Story = {
play: async ({ canvasElement, args }) => {
const body = within(canvasElement.ownerDocument.body);
const confirm = body.getByTestId("delete-dialog-name-confirmation");
const deleteButton = body.getByRole("button", { name: "Delete" });
await expect(deleteButton).toBeDisabled();
await userEvent.type(confirm, MockWorkspace.name);
await expect(deleteButton).toBeEnabled();
await userEvent.click(deleteButton);
await expect(args.onConfirm).toHaveBeenCalledWith(false);
},
};
// Should look the same as `Example`
export const Unhealthy: Story = {
@@ -48,10 +63,43 @@ export const UnhealthyAdminView: Story = {
workspace: MockFailedWorkspace,
canDeleteFailedWorkspace: true,
},
play: async ({ canvasElement, args }) => {
const body = within(canvasElement.ownerDocument.body);
const orphan = body.getByTestId("orphan-checkbox");
const confirm = body.getByTestId("delete-dialog-name-confirmation");
await userEvent.click(orphan);
await userEvent.type(confirm, MockFailedWorkspace.name);
await userEvent.click(body.getByRole("button", { name: "Delete" }));
await expect(args.onConfirm).toHaveBeenCalledWith(true);
},
};
export const WithTask: Story = {
args: {
workspace: MockTaskWorkspace,
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await expect(
body.getByText("This workspace is related to a task"),
).toBeInTheDocument();
await expect(
body.getByRole("link", { name: /this task/i }),
).toBeInTheDocument();
},
};
export const FilledWrong: Story = {
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const confirm = body.getByTestId("delete-dialog-name-confirmation");
await userEvent.type(confirm, "wrong-name");
await userEvent.tab();
await expect(
body.getByText("wrong-name does not match the name of this workspace"),
).toBeVisible();
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
},
};
@@ -1,16 +1,19 @@
import type { Interpolation, Theme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
import Link from "@mui/material/Link";
import TextField from "@mui/material/TextField";
import dayjs from "dayjs";
import { type FC, type FormEvent, useId, useState } from "react";
import type {
CreateWorkspaceBuildRequest,
Workspace,
} from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Checkbox } from "#/components/Checkbox/Checkbox";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { Input } from "#/components/Input/Input";
import { Label } from "#/components/Label/Label";
import { Link } from "#/components/Link/Link";
import { docs } from "#/utils/docs";
const warnBoxClassName =
"mt-6 flex gap-2 rounded-lg border border-solid border-border-warning bg-surface-orange p-3 leading-snug text-content-warning";
interface WorkspaceDeleteDialogProps {
workspace: Workspace;
canDeleteFailedWorkspace: boolean;
@@ -26,13 +29,19 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
onCancel,
onConfirm,
}) => {
const hookId = useId();
const confirmId = useId();
const errorId = `${confirmId}-error`;
const orphanId = `${confirmId}-orphan`;
const [userConfirmationText, setUserConfirmationText] = useState("");
const [orphanWorkspace, setOrphanWorkspace] =
useState<CreateWorkspaceBuildRequest["orphan"]>(false);
const [isFocused, setIsFocused] = useState(false);
const deletionConfirmed = workspace.name === userConfirmationText;
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
const displayErrorMessage = hasError && !isFocused;
const onSubmit = (event: FormEvent) => {
event.preventDefault();
if (deletionConfirmed) {
@@ -40,9 +49,6 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
}
};
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
const displayErrorMessage = hasError && !isFocused;
const inputColor = hasError ? "error" : "primary";
// Orphaning is sort of a "last resort" that should really only
// be used under the following circumstances:
// a) Terraform is failing to apply while deleting, which
@@ -69,14 +75,18 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
disabled={!deletionConfirmed}
description={
<>
<div css={styles.workspaceInfo}>
<div className="flex items-center justify-between rounded-md border border-solid border-border p-4 mb-5 leading-snug">
<div>
<p className="name">{workspace.name}</p>
<p className="label">workspace</p>
<p className="m-0 text-base font-semibold text-content-primary">
{workspace.name}
</p>
<p className="m-0 text-xs text-content-secondary">workspace</p>
</div>
<div className="text-right">
<p className="info">{dayjs(workspace.created_at).fromNow()}</p>
<p className="label">created</p>
<p className="m-0 text-xs font-medium text-content-primary">
{dayjs(workspace.created_at).fromNow()}
</p>
<p className="m-0 text-xs text-content-secondary">created</p>
</div>
</div>
@@ -86,39 +96,40 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
confirm:
</p>
<form onSubmit={onSubmit}>
<TextField
fullWidth
autoFocus
className="mt-8"
<form className="mt-2 flex flex-col gap-2" onSubmit={onSubmit}>
<Label htmlFor={confirmId}>Workspace name</Label>
<Input
id={confirmId}
name="confirmation"
autoComplete="off"
id={`${hookId}-confirm`}
autoFocus
placeholder={workspace.name}
value={userConfirmationText}
onChange={(event) => setUserConfirmationText(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
label="Workspace name"
color={inputColor}
error={displayErrorMessage}
helperText={
displayErrorMessage &&
`${userConfirmationText} does not match the name of this workspace`
}
InputProps={{ color: inputColor }}
inputProps={{
"data-testid": "delete-dialog-name-confirmation",
}}
aria-invalid={displayErrorMessage}
aria-describedby={displayErrorMessage ? errorId : undefined}
data-testid="delete-dialog-name-confirmation"
/>
{displayErrorMessage && (
<span id={errorId} className="text-xs text-content-destructive">
{userConfirmationText} does not match the name of this workspace
</span>
)}
{hasTask && (
<div css={styles.warnContainer}>
<div className="flex-col">
<p className="info">This workspace is related to a task</p>
<span className="text-xs mt-1 block">
<div className={warnBoxClassName}>
<div>
<p className="m-0 text-sm font-semibold">
This workspace is related to a task
</p>
<span className="mt-1 block text-xs text-content-secondary">
Deleting this workspace will also delete{" "}
<Link
href={`/tasks/${workspace.owner_name}/${workspace.task_id}`}
size="sm"
showExternalIcon={false}
>
this task
</Link>
@@ -127,39 +138,44 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
</div>
</div>
)}
{canOrphan && (
<div css={styles.warnContainer}>
<div className="flex-col">
<div className={warnBoxClassName}>
<label
htmlFor={orphanId}
className="flex items-start gap-2 cursor-pointer"
>
<Checkbox
id="orphan_resources"
size="small"
color="warning"
onChange={() => {
setOrphanWorkspace(!orphanWorkspace);
}}
className="option"
id={orphanId}
name="orphan_resources"
checked={orphanWorkspace}
onCheckedChange={(checked) => {
setOrphanWorkspace(checked === true);
}}
data-testid="orphan-checkbox"
className="mt-0.5 border-content-warning hover:enabled:border-content-warning data-[state=checked]:bg-content-warning data-[state=checked]:border-content-warning data-[state=checked]:text-content-invert hover:data-[state=checked]:bg-content-warning hover:data-[state=checked]:border-content-warning"
/>
</div>
<div className="flex-col">
<p className="info">Orphan Resources</p>
<span className="text-xs mt-1 block">
As a Template Admin, you may skip resource cleanup to delete
a failed workspace. Resources such as volumes and virtual
machines will not be destroyed.&nbsp;
<Link
href={docs(
"/user-guides/workspace-management#workspace-resources",
)}
target="_blank"
rel="noreferrer"
>
Learn more...
</Link>
<span>
<span className="block text-sm font-semibold">
Orphan Resources
</span>
<span className="mt-1 block text-xs text-content-secondary">
As a Template Admin, you may skip resource cleanup to
delete a failed workspace. Resources such as volumes and
virtual machines will not be destroyed.{" "}
<Link
href={docs(
"/user-guides/workspace-management#workspace-resources",
)}
target="_blank"
rel="noreferrer"
size="sm"
>
Learn more
</Link>
</span>
</span>
</div>
</label>
</div>
)}
</form>
@@ -168,56 +184,3 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
/>
);
};
const styles = {
workspaceInfo: (theme) => ({
display: "flex",
justifyContent: "space-between",
borderRadius: 6,
padding: 16,
marginBottom: 20,
lineHeight: "1.3em",
border: `1px solid ${theme.palette.divider}`,
"& .name": {
fontSize: 16,
fontWeight: 600,
color: theme.palette.text.primary,
},
"& .label": {
fontSize: 12,
color: theme.palette.text.secondary,
},
"& .info": {
fontSize: 12,
fontWeight: 500,
color: theme.palette.text.primary,
},
}),
warnContainer: (theme) => ({
marginTop: 24,
display: "flex",
backgroundColor: theme.roles.danger.background,
justifyContent: "space-between",
border: `1px solid ${theme.roles.danger.outline}`,
borderRadius: 8,
padding: 12,
gap: 8,
lineHeight: "18px",
"& .option": {
color: theme.roles.danger.fill.solid,
"&.Mui-checked": {
color: theme.roles.danger.fill.solid,
},
},
"& .info": {
fontSize: 14,
fontWeight: 600,
color: theme.roles.danger.text,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -9,6 +9,7 @@ import type {
WorkspaceBuild,
} from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
Dialog,
DialogContent,
@@ -17,7 +18,6 @@ import {
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { MemoizedInlineMarkdown } from "#/components/Markdown/InlineMarkdown";
type UseWorkspaceUpdateOptions = {
@@ -8,7 +8,7 @@ import {
deleteAIGatewayKeyMutation,
} from "#/api/queries/aiGatewayKeys";
import type { AIGatewayKey } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
@@ -1,8 +1,7 @@
import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
import type * as TypesGen from "#/api/typesGenerated";
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import type { useUnsavedChangesPrompt } from "#/hooks/useUnsavedChangesPrompt";
interface MCPServerFormDialogsProps {
@@ -25,13 +24,15 @@ export const MCPServerFormDialogs: FC<MCPServerFormDialogsProps> = ({
return (
<>
{server && onDeleteServer && (
<ConfirmDeleteDialog
<ConfirmDialog
type="delete"
open={confirmingDelete}
onOpenChange={setConfirmingDelete}
entity="MCP server"
onClose={() => setConfirmingDelete(false)}
title="Delete MCP server"
confirmText="Delete MCP server"
description={`Delete "${server.display_name}"? Agents will no longer be able to use this server.`}
onConfirm={() => void onDeleteServer(server.id)}
isPending={isDeleting}
confirmLoading={isDeleting}
/>
)}
<ConfirmDialog
@@ -2,6 +2,7 @@ import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
import type * as TypesGen from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
Dialog,
DialogContent,
@@ -10,7 +11,6 @@ import {
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
import type { ModelFormValues } from "#/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic";
export const ModelFormDialogs: FC<{
@@ -47,11 +47,14 @@ export const ModelFormDialogs: FC<{
return (
<>
{editingModel && onDeleteModel && (
<ConfirmDeleteDialog
entity="model"
isPending={isDeleting}
<ConfirmDialog
type="delete"
title="Delete model"
confirmText="Delete model"
description="Are you sure you want to delete this model? This action is irreversible."
confirmLoading={isDeleting}
open={confirmingDelete}
onOpenChange={(open) => !open && setConfirmingDelete(false)}
onClose={() => setConfirmingDelete(false)}
onConfirm={() => {
resetForm(formValues);
void onDeleteModel(editingModel.id);
@@ -14,7 +14,7 @@ import {
import { Avatar } from "#/components/Avatar/Avatar";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { Loader } from "#/components/Loader/Loader";
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
import { Switch } from "#/components/Switch/Switch";
@@ -10,7 +10,7 @@ import type {
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { Form, FormFields } from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
import { Label } from "#/components/Label/Label";
@@ -3,7 +3,7 @@ import { type FC, useId, useState } from "react";
import { getErrorMessage } from "#/api/errors";
import type { ChatUsageLimitGroupOverride, Group } from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { SearchField } from "#/components/SearchField/SearchField";
import { paginateItems } from "#/utils/paginateItems";
import { SpendSectionHeader } from "../SpendSectionHeader";
@@ -173,15 +173,18 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
)}
</div>
{pendingDeleteGroupId && (
<ConfirmDeleteDialog
entity="group override"
<ConfirmDialog
type="delete"
title="Delete group override"
confirmText="Delete group override"
description="Are you sure you want to delete this group override? This action is irreversible."
onConfirm={() => {
void onDeleteGroupOverride(pendingDeleteGroupId);
setPendingDeleteGroupId(null);
}}
isPending={deletePending}
confirmLoading={deletePending}
open
onOpenChange={(open) => !open && setPendingDeleteGroupId(null)}
onClose={() => setPendingDeleteGroupId(null)}
/>
)}
</section>
@@ -3,7 +3,7 @@ import { type FC, useId, useState } from "react";
import { getErrorMessage } from "#/api/errors";
import type { User } from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { SearchField } from "#/components/SearchField/SearchField";
import { paginateItems } from "#/utils/paginateItems";
import { SpendSectionHeader } from "../SpendSectionHeader";
@@ -154,15 +154,18 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
/>
</div>
{pendingDeleteUserId && (
<ConfirmDeleteDialog
entity="user override"
<ConfirmDialog
type="delete"
title="Delete user override"
confirmText="Delete user override"
description="Are you sure you want to delete this user override? This action is irreversible."
onConfirm={() => {
void onDeleteOverride(pendingDeleteUserId);
setPendingDeleteUserId(null);
}}
isPending={deletePending}
confirmLoading={deletePending}
open
onOpenChange={(open) => !open && setPendingDeleteUserId(null)}
onClose={() => setPendingDeleteUserId(null)}
/>
)}
</section>
@@ -7,7 +7,7 @@ import type {
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Input } from "#/components/Input/Input";
import { Loader } from "#/components/Loader/Loader";
@@ -3,6 +3,7 @@ import type { UserSkillMetadata } from "#/api/typesGenerated";
import { Alert, AlertDescription } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
Dialog,
DialogContent,
@@ -11,7 +12,6 @@ import {
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Loader } from "#/components/Loader/Loader";
import { Spinner } from "#/components/Spinner/Spinner";
@@ -168,35 +168,32 @@ const EditSkillDialog: FC<{
const DeleteSkillDialog: FC<{ state: PersonalSkillDeleteState }> = ({
state,
}) => {
const handleOpenChange = (open: boolean) => {
if (!open) {
state.onClose();
}
};
return (
<ConfirmDeleteDialog
<ConfirmDialog
type="delete"
open
onOpenChange={handleOpenChange}
entity="skill"
onClose={state.onClose}
title="Delete skill"
confirmText="Delete skill"
description={
<>
Delete {state.skill.name}? Agents will no longer be able to use this
skill. This action cannot be undone.
<p className="m-0">
Delete {state.skill.name}? Agents will no longer be able to use this
skill. This action cannot be undone.
</p>
{state.error && (
<Alert severity="error" className="mt-3">
<AlertDescription>
{state.error.message}
{state.error.detail ? ` ${state.error.detail}` : ""}
</AlertDescription>
</Alert>
)}
</>
}
onConfirm={state.onConfirm}
isPending={state.isDeleting}
>
{state.error && (
<Alert severity="error">
<AlertDescription>
{state.error.message}
{state.error.detail ? ` ${state.error.detail}` : ""}
</AlertDescription>
</Alert>
)}
</ConfirmDeleteDialog>
confirmLoading={state.isDeleting}
/>
);
};
@@ -16,7 +16,7 @@ import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { API } from "#/api/api";
import type * as TypesGen from "#/api/typesGenerated";
import type { Chat } from "#/api/typesGenerated";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { MockChat } from "#/testHelpers/chatEntities";
import {
MockNoPermissions,
@@ -48,8 +48,8 @@ import {
workspaceByIdKey,
} from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import {
getDefaultOrganizationName,
@@ -42,7 +42,7 @@ import {
CommandItem,
CommandList,
} from "#/components/Command/Command";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import {
Popover,
@@ -10,7 +10,7 @@ import {
} from "storybook/test";
import { API } from "#/api/api";
import type * as TypesGen from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { MockChatModelConfig } from "#/testHelpers/chatModels";
import {
MockDefaultOrganization,
@@ -9,7 +9,7 @@ import type { AgentChatSendShortcut } from "#/api/typesGenerated";
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { docs } from "#/utils/docs";
import { useFileAttachments } from "../hooks/useFileAttachments";
@@ -7,7 +7,7 @@ import { API } from "#/api/api";
import { getErrorDetail } from "#/api/errors";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { FullPageHorizontalForm } from "#/components/FullPageForm/FullPageHorizontalForm";
import { Loader } from "#/components/Loader/Loader";
import { pageTitle } from "#/utils/page";
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { expect, fn, userEvent, within } from "storybook/test";
import { AnnouncementBannerDialog } from "./AnnouncementBannerDialog";
const meta: Meta<typeof AnnouncementBannerDialog> = {
@@ -11,8 +11,8 @@ const meta: Meta<typeof AnnouncementBannerDialog> = {
message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
background_color: "#ffaff3",
},
onCancel: action("onCancel"),
onUpdate: () => Promise.resolve(void action("onUpdate")),
onCancel: fn(),
onUpdate: fn(async () => undefined),
},
};
@@ -22,3 +22,28 @@ type Story = StoryObj<typeof AnnouncementBannerDialog>;
const Example: Story = {};
export { Example as AnnouncementBannerDialog };
export const EditsMessage: Story = {
play: async ({ args }) => {
const body = within(document.body);
const message = await body.findByLabelText("Message");
await expect(message).toHaveValue(
"The beep-bop will be boop-beeped on Saturday at 12AM PST.",
);
await userEvent.clear(message);
await userEvent.type(message, "Scheduled maintenance tonight.");
await userEvent.click(body.getByRole("button", { name: "Update" }));
await expect(args.onUpdate).toHaveBeenCalledWith(
expect.objectContaining({ message: "Scheduled maintenance tonight." }),
);
},
};
export const CancelClosesDialog: Story = {
play: async ({ args }) => {
const body = within(document.body);
await userEvent.click(await body.findByRole("button", { name: "Cancel" }));
await expect(args.onCancel).toHaveBeenCalled();
},
};
@@ -1,12 +1,18 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import DialogActions from "@mui/material/DialogActions";
import { useTheme } from "@emotion/react";
import TextField from "@mui/material/TextField";
import { useFormik } from "formik";
import { type FC, useState } from "react";
import { SliderPicker, TwitterPicker } from "react-color";
import type { BannerConfig } from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { Dialog, DialogActionButtons } from "#/components/Dialogs/Dialog";
import {
Dialog,
DialogActions,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { AnnouncementBannerView } from "#/modules/dashboard/AnnouncementBanners/AnnouncementBannerView";
import { getFormHelpers } from "#/utils/formUtils";
@@ -38,20 +44,36 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
const [showHuePicker, setShowHuePicker] = useState(false);
return (
<Dialog css={styles.dialogWrapper} open onClose={onCancel}>
<Dialog
open
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onCancel();
}
}}
>
{/* Banner preview */}
<div className="fixed top-0 left-0 right-0">
<div className="fixed top-0 left-0 right-0 z-[60]">
<AnnouncementBannerView
message={bannerForm.values.message}
backgroundColor={bannerForm.values.background_color}
/>
</div>
<div css={styles.dialogContent}>
<h3 css={styles.dialogTitle}>Announcement banner</h3>
<DialogContent
className="max-w-[500px]"
data-testid="dialog"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>Announcement banner</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<div>
<h4 css={styles.settingName}>Message</h4>
<h4 className="m-0 mb-2 text-base font-semibold text-content-primary">
Message
</h4>
<TextField
{...bannerFieldHelpers("message", {
helperText: "Markdown bold, italics, and links are supported.",
@@ -65,7 +87,9 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
/>
</div>
<div>
<h4 css={styles.settingName}>Background color</h4>
<h4 className="m-0 mb-2 text-base font-semibold text-content-primary">
Background color
</h4>
<div className="flex flex-col gap-4">
{showHuePicker ? (
<SliderPicker
@@ -129,51 +153,18 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
</div>
</div>
</div>
</div>
<DialogActions>
<DialogActionButtons
cancelText="Cancel"
confirmLoading={bannerForm.isSubmitting}
confirmText="Update"
disabled={bannerForm.isSubmitting}
onCancel={onCancel}
onConfirm={bannerForm.handleSubmit}
/>
</DialogActions>
<DialogFooter>
<DialogActions
cancelText="Cancel"
confirmLoading={bannerForm.isSubmitting}
confirmText="Update"
confirmDisabled={bannerForm.isSubmitting}
onCancel={onCancel}
onConfirm={bannerForm.handleSubmit}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const styles = {
dialogWrapper: (theme) => ({
"& .MuiPaper-root": {
background: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
width: "100%",
maxWidth: 500,
},
"& .MuiDialogActions-spacing": {
padding: "0 40px 40px",
},
}),
dialogContent: (theme) => ({
color: theme.palette.text.secondary,
padding: "40px 40px 20px",
}),
dialogTitle: (theme) => ({
margin: 0,
marginBottom: 16,
color: theme.palette.text.primary,
fontWeight: 400,
fontSize: 20,
}),
settingName: (theme) => ({
marginTop: 0,
marginBottom: 8,
color: theme.palette.text.primary,
fontSize: 16,
lineHeight: "150%",
fontWeight: 600,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -2,7 +2,7 @@ import { PlusIcon } from "lucide-react";
import { type FC, useState } from "react";
import type { BannerConfig } from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Link } from "#/components/Link/Link";
import {
@@ -9,7 +9,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "#/components/Collapsible/Collapsible";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -19,8 +19,8 @@ import { Avatar } from "#/components/Avatar/Avatar";
import { Button } from "#/components/Button/Button";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import { CopyButton } from "#/components/CopyButton/CopyButton";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { Loader } from "#/components/Loader/Loader";
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
import { Spinner } from "#/components/Spinner/Spinner";
@@ -6,7 +6,7 @@ import * as Yup from "yup";
import type * as TypesGen from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { Form, FormFields } from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
import { Label } from "#/components/Label/Label";
+1 -1
View File
@@ -21,7 +21,7 @@ import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Avatar } from "#/components/Avatar/Avatar";
import { AvatarData } from "#/components/Avatar/AvatarData";
import { Button } from "#/components/Button/Button";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { useFilter } from "#/components/Filter/Filter";
import type { UsersFilter } from "#/components/Filter/UsersFilter";
import { Loader } from "#/components/Loader/Loader";
@@ -6,7 +6,7 @@ import { getErrorDetail, getErrorMessage } from "#/api/errors";
import { updateOrganization } from "#/api/queries/organizations";
import { deleteOrganizationRole, organizationRoles } from "#/api/queries/roles";
import type { Role } from "#/api/typesGenerated";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import {
SettingsHeader,
@@ -16,7 +16,7 @@ import type {
OrganizationMemberWithUserData,
User,
} from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { useFilter } from "#/components/Filter/Filter";
import { useAuthenticated } from "#/hooks/useAuthenticated";
@@ -8,7 +8,7 @@ import {
provisionerJobsQueryKey,
} from "#/api/queries/organizations";
import type { ProvisionerJob } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
type CancelJobConfirmationDialogProps = {
open: boolean;
@@ -12,7 +12,7 @@ import { Alert, AlertTitle } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { Checkbox } from "#/components/Checkbox/Checkbox";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import {
FormFields,
FormFooter,
@@ -3,7 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { ClockIcon, UserIcon } from "lucide-react";
import { type FC, type ReactNode, useState } from "react";
import type { Task } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
dayjs.extend(relativeTime);
@@ -21,8 +21,8 @@ import type {
} from "#/api/typesGenerated";
import { Avatar } from "#/components/Avatar/Avatar";
import { Button, Button as ShadcnButton } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,7 +8,7 @@ import {
templateVersions,
templateVersionsQueryKey,
} from "#/api/queries/templates";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { linkToTemplate, useLinks } from "#/modules/navigation";
import { useTemplateLayoutContext } from "#/pages/TemplatePage/TemplateLayout";
import { getTemplatePageTitle } from "../utils";
@@ -1,15 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { expect, fn, userEvent, within } from "storybook/test";
import { ScheduleDialog } from "./ScheduleDialog";
const meta: Meta<typeof ScheduleDialog> = {
title: "pages/TemplateSettingsPage/ScheduleDialog",
component: ScheduleDialog,
args: {
onConfirm: action("onConfirm"),
onClose: action("onClose"),
onConfirm: fn(),
onClose: fn(),
updateDormantWorkspaces: fn(),
updateInactiveWorkspaces: fn(),
open: true,
title: "Workspace Scheduling",
inactiveWorkspacesToGoDormant: 0,
inactiveWorkspacesToGoDormantInWeek: 0,
dormantWorkspacesToBeDeleted: 0,
dormantWorkspacesToBeDeletedInWeek: 0,
dormantWorkspacesChecked: false,
inactiveWorkspacesChecked: false,
dormantValueChanged: false,
deletionValueChanged: false,
},
};
@@ -31,3 +41,58 @@ export const DormancyDeletion: Story = {
dormantWorkspacesToBeDeletedInWeek: 5,
},
};
export const PreventDormancyAndSubmit: Story = {
args: {
dormantValueChanged: true,
inactiveWorkspacesToGoDormant: 1,
inactiveWorkspacesToGoDormantInWeek: 5,
},
play: async ({ args, step }) => {
const body = within(document.body);
await step("selecting prevention resets inactivity periods", async () => {
await userEvent.click(await body.findByRole("checkbox"));
await expect(args.updateInactiveWorkspaces).toHaveBeenCalledWith(true);
});
await step("submitting confirms the schedule change", async () => {
await userEvent.click(body.getByRole("button", { name: "Submit" }));
await expect(args.onConfirm).toHaveBeenCalled();
});
},
};
export const PreventDeletionAndSubmit: Story = {
args: {
deletionValueChanged: true,
dormantWorkspacesToBeDeleted: 1,
dormantWorkspacesToBeDeletedInWeek: 5,
},
play: async ({ args, step }) => {
const body = within(document.body);
await step("selecting prevention resets dormancy periods", async () => {
await userEvent.click(await body.findByRole("checkbox"));
await expect(args.updateDormantWorkspaces).toHaveBeenCalledWith(true);
});
await step("submitting confirms the schedule change", async () => {
await userEvent.click(body.getByRole("button", { name: "Submit" }));
await expect(args.onConfirm).toHaveBeenCalled();
});
},
};
export const CancelClosesDialog: Story = {
args: {
dormantValueChanged: true,
inactiveWorkspacesToGoDormant: 1,
inactiveWorkspacesToGoDormantInWeek: 5,
},
play: async ({ args }) => {
const body = within(document.body);
await userEvent.click(await body.findByRole("button", { name: "Cancel" }));
await expect(args.onClose).toHaveBeenCalled();
},
};
@@ -1,18 +1,35 @@
import type { Interpolation, Theme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
import DialogActions from "@mui/material/DialogActions";
import FormControlLabel from "@mui/material/FormControlLabel";
import type { FC } from "react";
import type { ConfirmDialogProps } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Dialog, DialogActionButtons } from "#/components/Dialogs/Dialog";
import { Checkbox } from "#/components/Checkbox/Checkbox";
import type { ConfirmDialogProps } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
Dialog,
DialogActions,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
interface ScheduleDialogProps extends ConfirmDialogProps {
interface ScheduleDialogProps
extends Pick<
ConfirmDialogProps,
| "open"
| "onClose"
| "onConfirm"
| "title"
| "cancelText"
| "confirmLoading"
| "disabled"
| "hideCancel"
> {
readonly inactiveWorkspacesToGoDormant: number;
readonly inactiveWorkspacesToGoDormantInWeek: number;
readonly dormantWorkspacesToBeDeleted: number;
readonly dormantWorkspacesToBeDeletedInWeek: number;
readonly updateDormantWorkspaces: (confirm: boolean) => void;
readonly updateInactiveWorkspaces: (confirm: boolean) => void;
readonly dormantWorkspacesChecked: boolean;
readonly inactiveWorkspacesChecked: boolean;
readonly dormantValueChanged: boolean;
readonly deletionValueChanged: boolean;
}
@@ -21,7 +38,7 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
cancelText,
confirmLoading,
disabled = false,
hideCancel,
hideCancel = false,
onClose,
onConfirm,
open = false,
@@ -32,18 +49,11 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
dormantWorkspacesToBeDeletedInWeek,
updateDormantWorkspaces,
updateInactiveWorkspaces,
dormantWorkspacesChecked,
inactiveWorkspacesChecked,
dormantValueChanged,
deletionValueChanged,
}) => {
const defaults = {
confirmText: "Delete",
hideCancel: false,
};
if (typeof hideCancel === "undefined") {
hideCancel = defaults.hideCancel;
}
const showDormancyWarning =
dormantValueChanged &&
(inactiveWorkspacesToGoDormant > 0 ||
@@ -55,126 +65,110 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
return (
<Dialog
css={styles.dialogWrapper}
onClose={onClose}
open={open}
data-testid="dialog"
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
>
<div css={styles.dialogContent}>
<h3 css={styles.dialogTitle}>{title}</h3>
<DialogContent
variant="destructive"
data-testid="dialog"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
{showDormancyWarning && (
<>
<h4>Dormancy Threshold</h4>
<p css={styles.dialogDescription}>
This change will result in{" "}
<strong>{inactiveWorkspacesToGoDormant}</strong>{" "}
{inactiveWorkspacesToGoDormant === 1 ? "workspace" : "workspaces"}{" "}
being immediately transitioned to the dormant state and{" "}
<strong>{inactiveWorkspacesToGoDormantInWeek}</strong>{" "}
{inactiveWorkspacesToGoDormantInWeek === 1
? "workspace"
: "workspaces"}{" "}
over the next 7 days. To prevent this, do you want to reset the
inactivity period for all template workspaces?
</p>
<FormControlLabel
className="mt-4"
control={
<div className="flex flex-col gap-4 text-sm text-content-secondary font-medium [&_strong]:text-content-primary">
{showDormancyWarning && (
<div className="flex flex-col gap-3">
<h4 className="m-0 text-base font-semibold text-content-primary">
Dormancy Threshold
</h4>
<p className="m-0 leading-relaxed">
This change will result in{" "}
<strong>{inactiveWorkspacesToGoDormant}</strong>{" "}
{inactiveWorkspacesToGoDormant === 1
? "workspace"
: "workspaces"}{" "}
being immediately transitioned to the dormant state and{" "}
<strong>{inactiveWorkspacesToGoDormantInWeek}</strong>{" "}
{inactiveWorkspacesToGoDormantInWeek === 1
? "workspace"
: "workspaces"}{" "}
over the next 7 days. To prevent this, do you want to reset the
inactivity period for all template workspaces?
</p>
<label
htmlFor="prevent-dormancy"
className="flex items-center gap-2 text-content-primary"
>
<Checkbox
size="small"
onChange={(e) => {
updateInactiveWorkspaces(e.target.checked);
id="prevent-dormancy"
checked={inactiveWorkspacesChecked}
onCheckedChange={(checked) => {
updateInactiveWorkspaces(checked === true);
}}
/>
}
label="Prevent Dormancy - Reset all workspace inactivity periods"
/>
</>
)}
<span>
Prevent Dormancy - Reset all workspace inactivity periods
</span>
</label>
</div>
)}
{showDeletionWarning && (
<>
<h4>Dormancy Auto-Deletion</h4>
<p css={styles.dialogDescription}>
This change will result in{" "}
<strong>{dormantWorkspacesToBeDeleted}</strong>{" "}
{dormantWorkspacesToBeDeleted === 1 ? "workspace" : "workspaces"}{" "}
being immediately deleted and{" "}
<strong>{dormantWorkspacesToBeDeletedInWeek}</strong>{" "}
{dormantWorkspacesToBeDeletedInWeek === 1
? "workspace"
: "workspaces"}{" "}
over the next 7 days. To prevent this, do you want to reset the
dormancy period for all template workspaces?
</p>
<FormControlLabel
className="mt-4"
control={
{showDeletionWarning && (
<div className="flex flex-col gap-3">
<h4 className="m-0 text-base font-semibold text-content-primary">
Dormancy Auto-Deletion
</h4>
<p className="m-0 leading-relaxed">
This change will result in{" "}
<strong>{dormantWorkspacesToBeDeleted}</strong>{" "}
{dormantWorkspacesToBeDeleted === 1
? "workspace"
: "workspaces"}{" "}
being immediately deleted and{" "}
<strong>{dormantWorkspacesToBeDeletedInWeek}</strong>{" "}
{dormantWorkspacesToBeDeletedInWeek === 1
? "workspace"
: "workspaces"}{" "}
over the next 7 days. To prevent this, do you want to reset the
dormancy period for all template workspaces?
</p>
<label
htmlFor="prevent-deletion"
className="flex items-center gap-2 text-content-primary"
>
<Checkbox
size="small"
onChange={(e) => {
updateDormantWorkspaces(e.target.checked);
id="prevent-deletion"
checked={dormantWorkspacesChecked}
onCheckedChange={(checked) => {
updateDormantWorkspaces(checked === true);
}}
/>
}
label="Prevent Deletion - Reset all workspace dormancy periods"
/>
</>
)}
</div>
<span>
Prevent Deletion - Reset all workspace dormancy periods
</span>
</label>
</div>
)}
</div>
<DialogActions>
<DialogActionButtons
cancelText={cancelText}
confirmLoading={confirmLoading}
confirmText="Submit"
disabled={disabled}
onCancel={!hideCancel ? onClose : undefined}
onConfirm={onConfirm || onClose}
type="delete"
/>
</DialogActions>
<DialogFooter>
<DialogActions
cancelText={cancelText}
confirmLoading={confirmLoading}
confirmText="Submit"
confirmDisabled={disabled}
confirmVariant="destructive"
onCancel={!hideCancel ? onClose : undefined}
onConfirm={onConfirm || onClose}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const styles = {
dialogWrapper: (theme) => ({
"& .MuiPaper-root": {
background: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
},
"& .MuiDialogActions-spacing": {
padding: "0 40px 40px",
},
}),
dialogContent: (theme) => ({
color: theme.palette.text.secondary,
padding: 40,
}),
dialogTitle: (theme) => ({
margin: 0,
marginBottom: 16,
color: theme.palette.text.primary,
fontWeight: 400,
fontSize: 20,
}),
dialogDescription: (theme) => ({
color: theme.palette.text.secondary,
lineHeight: "160%",
fontSize: 16,
"& strong": {
color: theme.palette.text.primary,
},
"& p:not(.MuiFormHelperText-root)": {
margin: 0,
},
"& > p": {
margin: "8px 0",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -618,6 +618,12 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
updateInactiveWorkspaces={(update: boolean) =>
form.setFieldValue("update_workspace_last_used_at", update)
}
dormantWorkspacesChecked={
form.values.update_workspace_dormant_at ?? false
}
inactiveWorkspacesChecked={
form.values.update_workspace_last_used_at ?? false
}
dormantValueChanged={
form.initialValues.time_til_dormant_ms !==
form.values.time_til_dormant_ms
@@ -1,6 +1,6 @@
import TextField from "@mui/material/TextField";
import { type ChangeEvent, type FC, useState } from "react";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { type FileTree, isFolder, validatePath } from "#/utils/filetree";
interface CreateFileDialogProps {
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { MockTemplateVersionVariable5 } from "#/testHelpers/entities";
import { MissingTemplateVariablesDialog } from "./MissingTemplateVariablesDialog";
const meta = {
title: "pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog",
component: MissingTemplateVariablesDialog,
args: {
open: true,
onClose: fn(),
onSubmit: fn(),
missingVariables: [MockTemplateVersionVariable5],
},
} satisfies Meta<typeof MissingTemplateVariablesDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Example: Story = {};
export const SubmitsEnteredValues: Story = {
play: async ({ args }) => {
const body = within(document.body);
const input = await body.findByRole("textbox");
await userEvent.type(input, "production");
await userEvent.click(body.getByRole("button", { name: "Submit" }));
await expect(args.onSubmit).toHaveBeenCalledWith([
{ name: "fifth_variable", value: "production" },
]);
},
};
@@ -1,20 +1,23 @@
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import { type FC, useEffect, useState } from "react";
import type {
TemplateVersionVariable,
VariableValue,
} from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import type { DialogProps } from "#/components/Dialogs/Dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { FormFields, VerticalForm } from "#/components/Form/Form";
import { Loader } from "#/components/Loader/Loader";
import { VariableInput } from "#/pages/CreateTemplatePage/VariableInput";
type MissingTemplateVariablesDialogProps = Omit<DialogProps, "onSubmit"> & {
type MissingTemplateVariablesDialogProps = {
open: boolean;
onClose: () => void;
onSubmit: (values: VariableValue[]) => void;
missingVariables?: TemplateVersionVariable[];
@@ -22,7 +25,7 @@ type MissingTemplateVariablesDialogProps = Omit<DialogProps, "onSubmit"> & {
export const MissingTemplateVariablesDialog: FC<
MissingTemplateVariablesDialogProps
> = ({ missingVariables, onSubmit, ...dialogProps }) => {
> = ({ missingVariables, onSubmit, open, onClose }) => {
const [variableValues, setVariableValues] = useState<VariableValue[]>([]);
// Pre-fill the form with the default values when missing variables are loaded
@@ -37,24 +40,23 @@ export const MissingTemplateVariablesDialog: FC<
return (
<Dialog
{...dialogProps}
scroll="body"
aria-labelledby="update-build-parameters-title"
maxWidth="xs"
data-testid="dialog"
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
>
<DialogTitle
id="update-build-parameters-title"
className="px-10 py-6 text-xl font-normal"
>
Template variables
</DialogTitle>
<DialogContent className="px-10">
<DialogContentText className="m-0">
There are a few missing template variable values. Please fill them in.
</DialogContentText>
<DialogContent className="max-w-md" data-testid="dialog">
<DialogHeader>
<DialogTitle>Template variables</DialogTitle>
<DialogDescription>
There are a few missing template variable values. Please fill them
in.
</DialogDescription>
</DialogHeader>
<VerticalForm
className="pt-8"
id="updateVariables"
onSubmit={(e) => {
e.preventDefault();
@@ -70,13 +72,9 @@ export const MissingTemplateVariablesDialog: FC<
variable={variable}
key={variable.name}
onChange={async (value) => {
setVariableValues((prev) => {
prev[index] = {
name: variable.name,
value,
};
return [...prev];
});
setVariableValues((prev) =>
prev.with(index, { name: variable.name, value }),
);
}}
/>
);
@@ -86,20 +84,16 @@ export const MissingTemplateVariablesDialog: FC<
<Loader />
)}
</VerticalForm>
<DialogFooter>
<Button variant="outline" type="button" onClick={onClose}>
Cancel
</Button>
<Button type="submit" form="updateVariables">
Submit
</Button>
</DialogFooter>
</DialogContent>
<DialogActions disableSpacing className="flex flex-col gap-2 p-10">
<Button className="w-full" type="submit" form="updateVariables">
Submit
</Button>
<Button
variant="outline"
className="w-full"
type="button"
onClick={dialogProps.onClose}
>
Cancel
</Button>
</DialogActions>
</Dialog>
);
};
@@ -5,8 +5,7 @@ import { useFormik } from "formik";
import type { FC } from "react";
import * as Yup from "yup";
import { EnterpriseBadge } from "#/components/Badges/Badges";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import type { DialogProps } from "#/components/Dialogs/Dialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { FormFields } from "#/components/Form/Form";
import {
HelpPopover,
@@ -21,7 +20,8 @@ import type { PublishVersionData } from "#/pages/TemplateVersionEditorPage/types
import { docs } from "#/utils/docs";
import { getFormHelpers } from "#/utils/formUtils";
type PublishTemplateVersionDialogProps = DialogProps & {
type PublishTemplateVersionDialogProps = {
open: boolean;
defaultName: string;
isPublishing: boolean;
publishingError?: unknown;
@@ -32,12 +32,12 @@ type PublishTemplateVersionDialogProps = DialogProps & {
export const PublishTemplateVersionDialog: FC<
PublishTemplateVersionDialogProps
> = ({
open,
onConfirm,
isPublishing,
onClose,
defaultName,
publishingError,
...dialogProps
}) => {
const form = useFormik({
initialValues: {
@@ -60,7 +60,7 @@ export const PublishTemplateVersionDialog: FC<
return (
<ConfirmDialog
{...dialogProps}
open={open}
confirmLoading={isPublishing}
onClose={handleClose}
onConfirm={async () => {
@@ -8,7 +8,7 @@ import {
validateExternalAuth,
} from "#/api/queries/externalAuth";
import type { ExternalAuthLinkProvider } from "#/api/typesGenerated";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import {
SettingsHeader,
SettingsHeaderTitle,
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { toast } from "sonner";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import { getApps, revokeApp } from "#/api/queries/oauth2";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import {
SettingsHeader,
SettingsHeaderTitle,
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { toast } from "sonner";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import { regenerateUserSSHKey, userSSHKey } from "#/api/queries/sshKeys";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
SettingsHeader,
SettingsHeaderTitle,
@@ -3,7 +3,7 @@ import { type FC, useRef, useState } from "react";
import type { UserSecret } from "#/api/typesGenerated";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -12,7 +12,7 @@ import type {
UserLoginType,
} from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import {
@@ -2,7 +2,7 @@ import type { FC } from "react";
import { toast } from "sonner";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import type { APIKeyWithOwner } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { useDeleteToken } from "./hooks";
interface ConfirmDeleteDialogProps {
@@ -1,7 +1,7 @@
import type { FC } from "react";
import type * as TypesGen from "#/api/typesGenerated";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
interface ResetPasswordDialogProps {
open: boolean;
+2 -2
View File
@@ -15,8 +15,8 @@ import {
updateRoles,
} from "#/api/queries/users";
import type { User } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { useFilter } from "#/components/Filter/Filter";
import { useStatusFilterMenu } from "#/components/Filter/UsersFilter";
import { useAuthenticated } from "#/hooks/useAuthenticated";
@@ -153,9 +153,7 @@ describe("WorkspacePage", () => {
await user.type(textField, MockFailedWorkspace.name);
// check orphan option
const orphanCheckbox = within(
screen.getByTestId("orphan-checkbox"),
).getByRole("checkbox");
const orphanCheckbox = screen.getByTestId("orphan-checkbox");
await user.click(orphanCheckbox);
@@ -22,7 +22,7 @@ import type * as TypesGen from "#/api/typesGenerated";
import {
ConfirmDialog,
type ConfirmDialogProps,
} from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
} from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { useWorkspaceBuildLogs } from "#/hooks/useWorkspaceBuildLogs";
import { EphemeralParametersDialog } from "#/modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog";
import { WorkspaceErrorDialog } from "#/modules/workspaces/ErrorDialog/WorkspaceErrorDialog";
@@ -11,7 +11,7 @@ import type {
WorkspaceBuildParameter,
} from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Link } from "#/components/Link/Link";
import { Loader } from "#/components/Loader/Loader";
@@ -10,7 +10,7 @@ import { workspaceByOwnerAndNameKey } from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
import { Alert } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { Link } from "#/components/Link/Link";
import { Loader } from "#/components/Loader/Loader";
import {
@@ -3,7 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { ClockIcon, UserIcon } from "lucide-react";
import { type FC, type ReactNode, useState } from "react";
import type { Workspace } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import { getResourceIconPath } from "#/utils/workspace";
@@ -1,6 +1,6 @@
import type { FC } from "react";
import type { Workspace } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
type BatchStopConfirmationProps = {
workspacesToStop: readonly Workspace[];
@@ -38,7 +38,7 @@ import { AvatarDataSkeleton } from "#/components/Avatar/AvatarDataSkeleton";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { Checkbox } from "#/components/Checkbox/Checkbox";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import { VSCodeIcon } from "#/components/Icons/VSCodeIcon";
import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon";