mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site/src/pages/AgentsPage): extract ConfirmDeleteDialog component (#24128)
This commit is contained in:
@@ -9,14 +9,6 @@ import { type FC, useState } from "react";
|
||||
import * as Yup from "yup";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import {
|
||||
InputGroup,
|
||||
@@ -41,6 +33,7 @@ import {
|
||||
import { cn } from "#/utils/cn";
|
||||
import { getFormHelpers } from "#/utils/formUtils";
|
||||
import { BackButton } from "../BackButton";
|
||||
import { ConfirmDeleteDialog } from "../ConfirmDeleteDialog";
|
||||
import type { ProviderState } from "./ChatModelAdminPanel";
|
||||
import {
|
||||
GeneralModelConfigFields,
|
||||
@@ -646,37 +639,13 @@ export const ModelForm: FC<ModelFormProps> = ({
|
||||
</div>
|
||||
</form>
|
||||
{editingModel && onDeleteModel && (
|
||||
<Dialog
|
||||
<ConfirmDeleteDialog
|
||||
entity="model"
|
||||
onConfirm={() => void onDeleteModel(editingModel.id)}
|
||||
isPending={isDeleting}
|
||||
open={confirmingDelete}
|
||||
onOpenChange={(open) => !open && setConfirmingDelete(false)}
|
||||
>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete model</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this model? This action is
|
||||
irreversible.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfirmingDelete(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void onDeleteModel(editingModel.id)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting && <Spinner className="h-4 w-4" loading />}
|
||||
Delete model
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
)}{" "}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,14 +10,6 @@ import {
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } 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 { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Switch } from "#/components/Switch/Switch";
|
||||
@@ -28,6 +20,7 @@ import {
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { formatProviderLabel } from "../../utils/modelOptions";
|
||||
import { BackButton } from "../BackButton";
|
||||
import { ConfirmDeleteDialog } from "../ConfirmDeleteDialog";
|
||||
import type { ProviderState } from "./ChatModelAdminPanel";
|
||||
import { readOptionalString } from "./helpers";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
@@ -266,7 +259,6 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-4 border-0 border-t border-solid border-border" />
|
||||
|
||||
{isAPIKeyEnvManaged ? (
|
||||
<Alert severity="info">
|
||||
<AlertTitle>API key managed by environment variable</AlertTitle>
|
||||
@@ -401,38 +393,15 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{providerConfig && (
|
||||
<Dialog
|
||||
<ConfirmDeleteDialog
|
||||
entity="provider"
|
||||
description={deleteProviderDescription}
|
||||
onConfirm={() => void onDeleteProvider(providerConfig.id)}
|
||||
isPending={isProviderMutationPending}
|
||||
open={confirmingDelete}
|
||||
onOpenChange={(open) => !open && setConfirmingDelete(false)}
|
||||
>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete provider</DialogTitle>
|
||||
<DialogDescription>{deleteProviderDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfirmingDelete(false)}
|
||||
disabled={isProviderMutationPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void onDeleteProvider(providerConfig.id)}
|
||||
disabled={isProviderMutationPending}
|
||||
>
|
||||
{isProviderMutationPending && (
|
||||
<Spinner className="h-4 w-4" loading />
|
||||
)}
|
||||
Delete provider
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
onConfirm: () => void;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmDeleteDialog: FC<ConfirmDeleteDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
entity,
|
||||
description,
|
||||
onConfirm,
|
||||
isPending = false,
|
||||
}) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete {entity}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{description ??
|
||||
`Are you sure you want to delete this ${entity}? This action is irreversible.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm} disabled={isPending}>
|
||||
{isPending && <Spinner className="h-4 w-4" loading />}
|
||||
Delete {entity}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -146,9 +146,7 @@ export const DeleteGroupOverride: Story = {
|
||||
const dialog = await body.findByRole("dialog");
|
||||
await expect(dialog).toBeInTheDocument();
|
||||
await expect(
|
||||
body.getByText(
|
||||
/Are you sure you want to delete this group limit override/i,
|
||||
),
|
||||
body.getByText(/Are you sure you want to delete this group override/i),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,14 +5,6 @@ import type { Group } from "#/api/typesGenerated";
|
||||
import { Autocomplete } from "#/components/Autocomplete/Autocomplete";
|
||||
import { AvatarData } from "#/components/Avatar/AvatarData";
|
||||
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";
|
||||
@@ -29,6 +21,7 @@ import {
|
||||
formatCostMicros,
|
||||
isPositiveFiniteDollarAmount,
|
||||
} from "#/utils/currency";
|
||||
import { ConfirmDeleteDialog } from "../ConfirmDeleteDialog";
|
||||
import { SectionHeader } from "../SectionHeader";
|
||||
|
||||
interface GroupLimitsSectionProps {
|
||||
@@ -298,40 +291,16 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{pendingDeleteGroupId && (
|
||||
<Dialog
|
||||
open
|
||||
<ConfirmDeleteDialog
|
||||
entity="group override"
|
||||
onConfirm={() => {
|
||||
void onDeleteGroupOverride(pendingDeleteGroupId);
|
||||
setPendingDeleteGroupId(null);
|
||||
}}
|
||||
isPending={deletePending}
|
||||
open={true}
|
||||
onOpenChange={(open) => !open && setPendingDeleteGroupId(null)}
|
||||
>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete group override</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this group limit override? This
|
||||
action is irreversible.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPendingDeleteGroupId(null)}
|
||||
disabled={deletePending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
void onDeleteGroupOverride(pendingDeleteGroupId);
|
||||
setPendingDeleteGroupId(null);
|
||||
}}
|
||||
disabled={deletePending}
|
||||
>
|
||||
{deletePending && <Spinner className="h-4 w-4" loading />}
|
||||
Delete override
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
)}{" "}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -123,9 +123,7 @@ export const DeleteUserOverride: Story = {
|
||||
const dialog = await body.findByRole("dialog");
|
||||
await expect(dialog).toBeInTheDocument();
|
||||
await expect(
|
||||
body.getByText(
|
||||
/Are you sure you want to delete this user limit override/i,
|
||||
),
|
||||
body.getByText(/Are you sure you want to delete this user override/i),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,14 +3,6 @@ import { getErrorMessage } from "#/api/errors";
|
||||
import type { User } from "#/api/typesGenerated";
|
||||
import { AvatarData } from "#/components/Avatar/AvatarData";
|
||||
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";
|
||||
@@ -27,6 +19,7 @@ import {
|
||||
formatCostMicros,
|
||||
isPositiveFiniteDollarAmount,
|
||||
} from "#/utils/currency";
|
||||
import { ConfirmDeleteDialog } from "../ConfirmDeleteDialog";
|
||||
import { SectionHeader } from "../SectionHeader";
|
||||
|
||||
interface UserOverridesSectionProps {
|
||||
@@ -257,40 +250,16 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{pendingDeleteUserId && (
|
||||
<Dialog
|
||||
open
|
||||
<ConfirmDeleteDialog
|
||||
entity="user override"
|
||||
onConfirm={() => {
|
||||
void onDeleteOverride(pendingDeleteUserId);
|
||||
setPendingDeleteUserId(null);
|
||||
}}
|
||||
isPending={deletePending}
|
||||
open={true}
|
||||
onOpenChange={(open) => !open && setPendingDeleteUserId(null)}
|
||||
>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete user override</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this user limit override? This
|
||||
action is irreversible.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPendingDeleteUserId(null)}
|
||||
disabled={deletePending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
void onDeleteOverride(pendingDeleteUserId);
|
||||
setPendingDeleteUserId(null);
|
||||
}}
|
||||
disabled={deletePending}
|
||||
>
|
||||
{deletePending && <Spinner className="h-4 w-4" loading />}
|
||||
Delete override
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
)}{" "}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -492,7 +492,9 @@ export const DeleteServerConfirmed: Story = {
|
||||
await userEvent.click(await body.findByRole("button", { name: /Sentry/ }));
|
||||
await userEvent.click(await body.findByRole("button", { name: "Delete" }));
|
||||
await body.findByText(/Are you sure you want to delete this MCP server/i);
|
||||
await userEvent.click(body.getByRole("button", { name: /Delete server/i }));
|
||||
await userEvent.click(
|
||||
body.getByRole("button", { name: /Delete MCP server/i }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onDeleteServer).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -13,14 +13,6 @@ import { useSearchParams } from "react-router";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { IconField } from "#/components/IconField/IconField";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
@@ -42,6 +34,7 @@ import {
|
||||
import { cn } from "#/utils/cn";
|
||||
import { BackButton } from "./BackButton";
|
||||
import { ProviderField as Field } from "./ChatModelAdminPanel/ProviderForm";
|
||||
import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────
|
||||
@@ -842,37 +835,13 @@ const ServerForm: FC<ServerFormProps> = ({
|
||||
</div>
|
||||
</form>
|
||||
{server && (
|
||||
<Dialog
|
||||
<ConfirmDeleteDialog
|
||||
entity="MCP server"
|
||||
onConfirm={() => void onDelete(server.id)}
|
||||
isPending={isDeleting}
|
||||
open={confirmingDelete}
|
||||
onOpenChange={(open) => !open && setConfirmingDelete(false)}
|
||||
>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete server</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this MCP server? This action is
|
||||
irreversible.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfirmingDelete(false)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void onDelete(server.id)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isDeleting && <Spinner className="h-4 w-4" loading />}
|
||||
Delete server
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
)}{" "}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user