refactor(site/src/pages/AgentsPage): extract ConfirmDeleteDialog component (#24128)

This commit is contained in:
Danielle Maywood
2026-04-08 11:07:39 +01:00
committed by GitHub
parent 3f7a3e3354
commit 3a612898c6
9 changed files with 103 additions and 201 deletions
@@ -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>
);