diff --git a/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.stories.tsx b/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.stories.tsx new file mode 100644 index 00000000000..a80aa69c6e8 --- /dev/null +++ b/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.stories.tsx @@ -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 = { + title: "components/Dialog/ConfirmDialog", + component: ConfirmDialog, + args: { + onClose: fn(), + onConfirm: fn(), + open: true, + title: "Confirm Dialog", + }, +}; + +export default meta; +type Story = StoryObj; + +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(); + }, +}; diff --git a/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 00000000000..afc69d2c9ce --- /dev/null +++ b/site/src/components/Dialog/ConfirmDialog/ConfirmDialog.tsx @@ -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 = ({ + 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 ( + { + if (!nextOpen) { + onClose(); + } + }} + > + + + {title} + +
+ {description} +
+
+
+ + + + +
+
+ ); +}; diff --git a/site/src/components/Dialog/DeleteDialog/DeleteDialog.stories.tsx b/site/src/components/Dialog/DeleteDialog/DeleteDialog.stories.tsx new file mode 100644 index 00000000000..9c877fd0cc1 --- /dev/null +++ b/site/src/components/Dialog/DeleteDialog/DeleteDialog.stories.tsx @@ -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 = { + 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; + +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(); + }, +}; diff --git a/site/src/components/Dialog/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialog/DeleteDialog/DeleteDialog.tsx new file mode 100644 index 00000000000..5bceb5d1435 --- /dev/null +++ b/site/src/components/Dialog/DeleteDialog/DeleteDialog.tsx @@ -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 = ({ + 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) => { + event.preventDefault(); + if (deletionConfirmed && !confirmLoading) { + onConfirm(); + } + }; + + return ( + + + + {title ?? `Delete ${entity}`} + + {verb ?? "Deleting"} this {entity} is irreversible! + + + +
+ {info && ( + + {info} + + )} +

+ Type {name} below + to confirm. +

+
+ +
+
+ + 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 && ( + + {userConfirmationText} does not match the name of this {entity} + + )} +
+ + + + + +
+
+
+ ); +}; diff --git a/site/src/components/Dialog/Dialog.tsx b/site/src/components/Dialog/Dialog.tsx index d1cbfeb10b8..bdee36dd915 100644 --- a/site/src/components/Dialog/Dialog.tsx +++ b/site/src/components/Dialog/Dialog.tsx @@ -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 diff --git a/site/src/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog.tsx b/site/src/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog.tsx deleted file mode 100644 index 4df6b9af971..00000000000 --- a/site/src/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog.tsx +++ /dev/null @@ -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 = ({ - open, - onOpenChange, - entity, - description, - children, - onConfirm, - isPending = false, -}) => ( - - - - Delete {entity} - - {description ?? - `Are you sure you want to delete this ${entity}? This action is irreversible.`} - - - {children} - - - - - - -); diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx deleted file mode 100644 index 99895cb5b95..00000000000 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { action } from "storybook/actions"; -import { ConfirmDialog } from "./ConfirmDialog"; - -const meta: Meta = { - title: "components/Dialogs/ConfirmDialog", - component: ConfirmDialog, - args: { - onClose: action("onClose"), - onConfirm: action("onConfirm"), - open: true, - title: "Confirm Dialog", - }, -}; - -export default meta; -type Story = StoryObj; - -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, - }, -}; diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx deleted file mode 100644 index 96373e7b326..00000000000 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx +++ /dev/null @@ -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(); - 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(); - fireEvent.click(screen.getByText("CONFIRM")); - - // Then - expect(onCloseMock).toBeCalledTimes(0); - expect(onConfirmMock).toBeCalledTimes(1); - }); -}); diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx deleted file mode 100644 index 1b253ad848c..00000000000 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ /dev/null @@ -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 { - 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>; - -/** - * 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 = ({ - 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 ( - -
-

{title}

- {description &&
{description}
} -
- - - - -
- ); -}; diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx deleted file mode 100644 index dc3bbe9f2de..00000000000 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx +++ /dev/null @@ -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 = { - 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; - -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, - }, -}; diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx deleted file mode 100644 index 29b0c84a064..00000000000 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx +++ /dev/null @@ -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( - , - ); - - const confirmButton = screen.getByRole("button", { name: "Delete" }); - expect(confirmButton).toBeDisabled(); - }); - - it("disables confirm button when the text field is filled incorrectly", async () => { - renderComponent( - , - ); - - 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( - , - ); - - const textField = screen.getByTestId(inputTestId); - await fillInputField(textField, "MyTemplate"); - - const confirmButton = screen.getByRole("button", { name: "Delete" }); - expect(confirmButton).not.toBeDisabled(); - }); -}); diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx deleted file mode 100644 index 209aaf0ffe0..00000000000 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ /dev/null @@ -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 = ({ - 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 ( - -
-

- {verb ?? "Deleting"} this {entity} is irreversible! -

- {Boolean(info) && ( - - {info} - - )} -

- Type {name} below to confirm. -

-
- -
- 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", - }} - /> - - - } - /> - ); -}; diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx deleted file mode 100644 index f8fd9adce93..00000000000 --- a/site/src/components/Dialogs/Dialog.tsx +++ /dev/null @@ -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 = ({ - cancelText = "Cancel", - confirmText = "Confirm", - confirmLoading = false, - disabled = false, - onCancel, - onConfirm, - type = "info", -}) => { - return ( - <> - {onCancel && ( - - )} - - {onConfirm && ( - - )} - - ); -}; - -/** - * 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 }; diff --git a/site/src/components/Dialogs/types.ts b/site/src/components/Dialogs/types.ts deleted file mode 100644 index 4e3444811f2..00000000000 --- a/site/src/components/Dialogs/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type ConfirmDialogType = "delete" | "info" | "success"; diff --git a/site/src/modules/resources/AgentDevcontainerMoreActions.tsx b/site/src/modules/resources/AgentDevcontainerMoreActions.tsx index c2158bc873f..f668e67ab97 100644 --- a/site/src/modules/resources/AgentDevcontainerMoreActions.tsx +++ b/site/src/modules/resources/AgentDevcontainerMoreActions.tsx @@ -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, diff --git a/site/src/modules/tasks/TaskDeleteDialog/TaskDeleteDialog.tsx b/site/src/modules/tasks/TaskDeleteDialog/TaskDeleteDialog.tsx index a7aefaa9ca4..b9c9b49481a 100644 --- a/site/src/modules/tasks/TaskDeleteDialog/TaskDeleteDialog.tsx +++ b/site/src/modules/tasks/TaskDeleteDialog/TaskDeleteDialog.tsx @@ -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; diff --git a/site/src/modules/tasks/TaskFeedbackDialog/TaskFeedbackDialog.tsx b/site/src/modules/tasks/TaskFeedbackDialog/TaskFeedbackDialog.tsx index 42ce4ab4320..05917ca10ca 100644 --- a/site/src/modules/tasks/TaskFeedbackDialog/TaskFeedbackDialog.tsx +++ b/site/src/modules/tasks/TaskFeedbackDialog/TaskFeedbackDialog.tsx @@ -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 & { taskId: string; }; diff --git a/site/src/modules/workspaces/WorkspaceBuildCancelDialog/WorkspaceBuildCancelDialog.tsx b/site/src/modules/workspaces/WorkspaceBuildCancelDialog/WorkspaceBuildCancelDialog.tsx index 40e9150caba..54fb5258888 100644 --- a/site/src/modules/workspaces/WorkspaceBuildCancelDialog/WorkspaceBuildCancelDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildCancelDialog/WorkspaceBuildCancelDialog.tsx @@ -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; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx index d0709b8ba8f..8753627151a 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx @@ -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 ( { if (newVersion) { diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx index 004913c8ecb..802c320da5d 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx @@ -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"; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx index 000d738b465..4e13c723941 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx @@ -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 = { }, canDeleteFailedWorkspace: false, isOpen: true, + onCancel: fn(), + onConfirm: fn(), }, }; export default meta; type Story = StoryObj; -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(); + }, }; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx index ba41958e187..ef4df7bd9a4 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx @@ -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 = ({ onCancel, onConfirm, }) => { - const hookId = useId(); + const confirmId = useId(); + const errorId = `${confirmId}-error`; + const orphanId = `${confirmId}-orphan`; + const [userConfirmationText, setUserConfirmationText] = useState(""); const [orphanWorkspace, setOrphanWorkspace] = useState(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 = ({ } }; - 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 = ({ disabled={!deletionConfirmed} description={ <> -
+
-

{workspace.name}

-

workspace

+

+ {workspace.name} +

+

workspace

-

{dayjs(workspace.created_at).fromNow()}

-

created

+

+ {dayjs(workspace.created_at).fromNow()} +

+

created

@@ -86,39 +96,40 @@ export const WorkspaceDeleteDialog: FC = ({ confirm:

-
- + + 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 && ( + + {userConfirmationText} does not match the name of this workspace + + )} + {hasTask && ( -
-
-

This workspace is related to a task

- +
+
+

+ This workspace is related to a task +

+ Deleting this workspace will also delete{" "} this task @@ -127,39 +138,44 @@ export const WorkspaceDeleteDialog: FC = ({
)} + {canOrphan && ( -
-
+
+
-
-

Orphan Resources

- - 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.  - - Learn more... - + + + Orphan Resources + + + 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.{" "} + + Learn more + + -
+
)} @@ -168,56 +184,3 @@ export const WorkspaceDeleteDialog: FC = ({ /> ); }; - -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>; diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 0ff03f61110..69cbe19e5c8 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -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 = { diff --git a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPage.tsx b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPage.tsx index e6f604dc59c..f34f1f193c1 100644 --- a/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPage.tsx +++ b/site/src/pages/AISettingsPage/GatewayKeysPage/GatewayKeysPage.tsx @@ -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"; diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormDialogs.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormDialogs.tsx index 86b86e31f1e..75338d6fa53 100644 --- a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormDialogs.tsx +++ b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormDialogs.tsx @@ -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 = ({ return ( <> {server && onDeleteServer && ( - 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} /> )} {editingModel && onDeleteModel && ( - !open && setConfirmingDelete(false)} + onClose={() => setConfirmingDelete(false)} onConfirm={() => { resetForm(formValues); void onDeleteModel(editingModel.id); diff --git a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx index fe0703edbf1..5a0b242d24f 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx @@ -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"; diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx index 982237df9b1..f701a4185ce 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx @@ -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"; diff --git a/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/GroupLimitsSection.tsx b/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/GroupLimitsSection.tsx index b0914b6e6bf..d0d912b4440 100644 --- a/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/GroupLimitsSection.tsx +++ b/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/GroupLimitsSection.tsx @@ -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 = ({ )}
{pendingDeleteGroupId && ( - { void onDeleteGroupOverride(pendingDeleteGroupId); setPendingDeleteGroupId(null); }} - isPending={deletePending} + confirmLoading={deletePending} open - onOpenChange={(open) => !open && setPendingDeleteGroupId(null)} + onClose={() => setPendingDeleteGroupId(null)} /> )} diff --git a/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/UserOverridesSection.tsx b/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/UserOverridesSection.tsx index 15b8c4832e9..882f4df2ad9 100644 --- a/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/UserOverridesSection.tsx +++ b/site/src/pages/AISettingsPage/SpendPage/components/LimitsTab/UserOverridesSection.tsx @@ -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 = ({ />
{pendingDeleteUserId && ( - { void onDeleteOverride(pendingDeleteUserId); setPendingDeleteUserId(null); }} - isPending={deletePending} + confirmLoading={deletePending} open - onOpenChange={(open) => !open && setPendingDeleteUserId(null)} + onClose={() => setPendingDeleteUserId(null)} /> )} diff --git a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx index fe7fd73b34b..646376bbdb1 100644 --- a/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx @@ -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"; diff --git a/site/src/pages/AgentsPage/AgentSettingsPersonalSkillsPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsPersonalSkillsPageView.tsx index dfe090605e9..cb843191c85 100644 --- a/site/src/pages/AgentsPage/AgentSettingsPersonalSkillsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsPersonalSkillsPageView.tsx @@ -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 ( - - Delete {state.skill.name}? Agents will no longer be able to use this - skill. This action cannot be undone. +

+ Delete {state.skill.name}? Agents will no longer be able to use this + skill. This action cannot be undone. +

+ {state.error && ( + + + {state.error.message} + {state.error.detail ? ` ${state.error.detail}` : ""} + + + )} } onConfirm={state.onConfirm} - isPending={state.isDeleting} - > - {state.error && ( - - - {state.error.message} - {state.error.detail ? ` ${state.error.detail}` : ""} - - - )} -
+ confirmLoading={state.isDeleting} + /> ); }; diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx index fd664b3e6da..1ff2a42f2f7 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.stories.tsx @@ -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, diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.tsx index d3a16c2e2bc..c8266076eb3 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.tsx @@ -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, diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index dfe684ad313..7fddd7c95ad 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -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, diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index 56524349131..886cd3475d4 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -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, diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index 7ce7a50971f..cf8b15c4b6e 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -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"; diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx index 6fe7a4d3f7a..63081744fff 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx @@ -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"; diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.stories.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.stories.tsx index 1c2a3e9de61..cc676c9696e 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.stories.tsx @@ -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 = { @@ -11,8 +11,8 @@ const meta: Meta = { 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; 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(); + }, +}; diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.tsx index 0b86ab3a28b..ccf1e6d993a 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerDialog.tsx @@ -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 = ({ const [showHuePicker, setShowHuePicker] = useState(false); return ( - + { + if (!nextOpen) { + onCancel(); + } + }} + > {/* Banner preview */} -
+
-
-

Announcement banner

+ + + Announcement banner + +
-

Message

+

+ Message +

= ({ />
-

Background color

+

+ Background color +

{showHuePicker ? ( = ({
-
- - - + + + +
); }; - -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>; diff --git a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerSettings.tsx b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerSettings.tsx index f3126a44e0a..bd9f5872562 100644 --- a/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerSettings.tsx +++ b/site/src/pages/DeploymentSettingsPage/AppearanceSettingsPage/AnnouncementBannerSettings.tsx @@ -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 { diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx index 0a09f189e77..b4667e4c9a4 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx @@ -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, diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx index c30d942beff..1768685fb1d 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx @@ -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"; diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx index 2e3d8fc3300..be100cb5e5d 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppForm.tsx @@ -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"; diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index 5778fbfc6e9..331730132ae 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -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"; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 03174f8bb79..12339d9c8bf 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -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, diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index 524e1d28f26..ed378fbf2a6 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -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"; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx index 22a0a1b9f2f..0e2302c56c5 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/CancelJobConfirmationDialog.tsx @@ -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; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index 2ccf13bc37c..a2d6c542d3d 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -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, diff --git a/site/src/pages/TasksPage/BatchDeleteConfirmation.tsx b/site/src/pages/TasksPage/BatchDeleteConfirmation.tsx index d7b5769ecd4..534de0558a0 100644 --- a/site/src/pages/TasksPage/BatchDeleteConfirmation.tsx +++ b/site/src/pages/TasksPage/BatchDeleteConfirmation.tsx @@ -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); diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index c8792aa7a6b..82c0d9e08b3 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -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, diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx index d8dc3b88ef8..36ad4df2ba1 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx @@ -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"; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.stories.tsx index 7b73781b864..df23a251aa6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.stories.tsx @@ -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 = { 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(); + }, +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.tsx index c696ab2f58d..41187cca93e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/ScheduleDialog.tsx @@ -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 = ({ cancelText, confirmLoading, disabled = false, - hideCancel, + hideCancel = false, onClose, onConfirm, open = false, @@ -32,18 +49,11 @@ export const ScheduleDialog: FC = ({ 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 = ({ return ( { + if (!nextOpen) { + onClose(); + } + }} > -
-

{title}

+ + + {title} + - {showDormancyWarning && ( - <> -

Dormancy Threshold

-

- This change will result in{" "} - {inactiveWorkspacesToGoDormant}{" "} - {inactiveWorkspacesToGoDormant === 1 ? "workspace" : "workspaces"}{" "} - being immediately transitioned to the dormant state and{" "} - {inactiveWorkspacesToGoDormantInWeek}{" "} - {inactiveWorkspacesToGoDormantInWeek === 1 - ? "workspace" - : "workspaces"}{" "} - over the next 7 days. To prevent this, do you want to reset the - inactivity period for all template workspaces? -

- + {showDormancyWarning && ( +
+

+ Dormancy Threshold +

+

+ This change will result in{" "} + {inactiveWorkspacesToGoDormant}{" "} + {inactiveWorkspacesToGoDormant === 1 + ? "workspace" + : "workspaces"}{" "} + being immediately transitioned to the dormant state and{" "} + {inactiveWorkspacesToGoDormantInWeek}{" "} + {inactiveWorkspacesToGoDormantInWeek === 1 + ? "workspace" + : "workspaces"}{" "} + over the next 7 days. To prevent this, do you want to reset the + inactivity period for all template workspaces? +

+ +
+ )} - {showDeletionWarning && ( - <> -

Dormancy Auto-Deletion

-

- This change will result in{" "} - {dormantWorkspacesToBeDeleted}{" "} - {dormantWorkspacesToBeDeleted === 1 ? "workspace" : "workspaces"}{" "} - being immediately deleted and{" "} - {dormantWorkspacesToBeDeletedInWeek}{" "} - {dormantWorkspacesToBeDeletedInWeek === 1 - ? "workspace" - : "workspaces"}{" "} - over the next 7 days. To prevent this, do you want to reset the - dormancy period for all template workspaces? -

- +

+ Dormancy Auto-Deletion +

+

+ This change will result in{" "} + {dormantWorkspacesToBeDeleted}{" "} + {dormantWorkspacesToBeDeleted === 1 + ? "workspace" + : "workspaces"}{" "} + being immediately deleted and{" "} + {dormantWorkspacesToBeDeletedInWeek}{" "} + {dormantWorkspacesToBeDeletedInWeek === 1 + ? "workspace" + : "workspaces"}{" "} + over the next 7 days. To prevent this, do you want to reset the + dormancy period for all template workspaces? +

+
+ + Prevent Deletion - Reset all workspace dormancy periods + + +
+ )} +
- - - + + + + ); }; - -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>; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index c07da6a1aa2..95f7f93b40b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -618,6 +618,12 @@ export const TemplateScheduleForm: FC = ({ 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 diff --git a/site/src/pages/TemplateVersionEditorPage/FileDialog.tsx b/site/src/pages/TemplateVersionEditorPage/FileDialog.tsx index f9a5b03c019..ef0d5ec17c9 100644 --- a/site/src/pages/TemplateVersionEditorPage/FileDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/FileDialog.tsx @@ -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 { diff --git a/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.stories.tsx b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.stories.tsx new file mode 100644 index 00000000000..40e180942fc --- /dev/null +++ b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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" }, + ]); + }, +}; diff --git a/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx index 136e75ddfcf..e3b16279d45 100644 --- a/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog.tsx @@ -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 & { +type MissingTemplateVariablesDialogProps = { + open: boolean; onClose: () => void; onSubmit: (values: VariableValue[]) => void; missingVariables?: TemplateVersionVariable[]; @@ -22,7 +25,7 @@ type MissingTemplateVariablesDialogProps = Omit & { export const MissingTemplateVariablesDialog: FC< MissingTemplateVariablesDialogProps -> = ({ missingVariables, onSubmit, ...dialogProps }) => { +> = ({ missingVariables, onSubmit, open, onClose }) => { const [variableValues, setVariableValues] = useState([]); // Pre-fill the form with the default values when missing variables are loaded @@ -37,24 +40,23 @@ export const MissingTemplateVariablesDialog: FC< return ( { + if (!nextOpen) { + onClose(); + } + }} > - - Template variables - - - - There are a few missing template variable values. Please fill them in. - + + + Template variables + + There are a few missing template variable values. Please fill them + in. + + + { 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< )} + + + + + - - - - ); }; diff --git a/site/src/pages/TemplateVersionEditorPage/PublishTemplateVersionDialog.tsx b/site/src/pages/TemplateVersionEditorPage/PublishTemplateVersionDialog.tsx index 63af38e28e4..b496eee234c 100644 --- a/site/src/pages/TemplateVersionEditorPage/PublishTemplateVersionDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/PublishTemplateVersionDialog.tsx @@ -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 ( { diff --git a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPage.tsx b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPage.tsx index 33903dc8118..a625c781beb 100644 --- a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPage.tsx +++ b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPage.tsx @@ -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, diff --git a/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage.tsx b/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage.tsx index 752c9ad76ac..a78d8651d67 100644 --- a/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage.tsx +++ b/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage.tsx @@ -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, diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx index faec8cd3f03..007dbee440a 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx @@ -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, diff --git a/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx b/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx index ac1966f9b1d..60351a54107 100644 --- a/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx +++ b/site/src/pages/UserSettingsPage/SecretsPage/SecretsTable.tsx @@ -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, diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index 7598bfbbf55..bd2a50fa9b8 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -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 { diff --git a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx index 4b918fbfbab..c2b114e77c9 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx @@ -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 { diff --git a/site/src/pages/UsersPage/ResetPasswordDialog.tsx b/site/src/pages/UsersPage/ResetPasswordDialog.tsx index 7e9e3ecc765..5860816562e 100644 --- a/site/src/pages/UsersPage/ResetPasswordDialog.tsx +++ b/site/src/pages/UsersPage/ResetPasswordDialog.tsx @@ -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; diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 3e70e8f01fd..2151dc954db 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -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"; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index f6affdd87ce..31c901af8db 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -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); diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 0f1166fbed9..d390ea2bb35 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -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"; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 26275c5aaf7..20f277eb391 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -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"; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 5ef83950494..68c2ab0fd3e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -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 { diff --git a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx index 16beacf1b3e..48ea9ebe730 100644 --- a/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchDeleteConfirmation.tsx @@ -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"; diff --git a/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx index 6cee45d1455..031a4353a74 100644 --- a/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx @@ -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[]; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index f91b90d244a..adf8518eb3a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -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";