Skip to content

Commit 2692259

Browse files
feat(deployments): type-to-confirm dialog for deployment deletion (#12546)
* feat(deployments): replace simple delete confirm with type-to-confirm dialog Deleting a deployment is irreversible — it removes the agent from both Langflow and Watsonx Orchestrate permanently. Require the user to type the deployment name before the Delete button activates, matching industry-standard patterns (GitHub, AWS). - Add `TypeToConfirmDeleteDialog` component (deployment-specific, not shared) - Input resets on close; label + placeholder for accessibility - Replace `DeleteConfirmationModal` in `deployments-content.tsx` - 6 unit tests covering all confirmation behaviours * fix(deployments): address PR review findings on type-to-confirm dialog - Fix icon spacing: replace pr-1 with mr-2 on AlertTriangle (padding was compressing the SVG viewport instead of creating sibling spacing) - Fix label copy: "agent name" → "deployment name" to cover both agent and MCP deployment types - Remove unused cancelDelete from useDeleteWithConfirmation — callers close the dialog via setModalOpen; the export was dead code - Add case-sensitivity test to type-to-confirm-delete-dialog tests - Add unit tests for useDeleteWithConfirmation hook (8 tests covering requestDelete, confirmDelete, onSettled, onError, and setModalOpen)
1 parent 6d7715b commit 2692259

5 files changed

Lines changed: 399 additions & 15 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import { TooltipProvider } from "@/components/ui/tooltip";
3+
import TypeToConfirmDeleteDialog from "../components/type-to-confirm-delete-dialog";
4+
5+
function renderDialog(
6+
props: React.ComponentProps<typeof TypeToConfirmDeleteDialog>,
7+
) {
8+
return render(
9+
<TooltipProvider>
10+
<TypeToConfirmDeleteDialog {...props} />
11+
</TooltipProvider>,
12+
);
13+
}
14+
15+
function rerenderDialog(
16+
rerender: ReturnType<typeof render>["rerender"],
17+
props: React.ComponentProps<typeof TypeToConfirmDeleteDialog>,
18+
) {
19+
return rerender(
20+
<TooltipProvider>
21+
<TypeToConfirmDeleteDialog {...props} />
22+
</TooltipProvider>,
23+
);
24+
}
25+
26+
const defaultProps = {
27+
open: true,
28+
onOpenChange: jest.fn(),
29+
deploymentName: "My Agent",
30+
onConfirm: jest.fn(),
31+
};
32+
33+
describe("TypeToConfirmDeleteDialog", () => {
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
});
37+
38+
it("renders deployment name in the body", () => {
39+
renderDialog(defaultProps);
40+
41+
expect(
42+
screen.getByText(/Permanently delete the deployment/),
43+
).toBeInTheDocument();
44+
expect(screen.getByRole("code")).toHaveTextContent("My Agent");
45+
});
46+
47+
it("Delete button is disabled when input is empty", () => {
48+
renderDialog(defaultProps);
49+
50+
const deleteBtn = screen.getByTestId("btn-delete-type-to-confirm-delete");
51+
expect(deleteBtn).toBeDisabled();
52+
});
53+
54+
it("Delete button is disabled when input does not match deployment name", () => {
55+
renderDialog(defaultProps);
56+
57+
const input = screen.getByTestId("input-type-to-confirm-delete");
58+
fireEvent.change(input, { target: { value: "wrong-value" } });
59+
60+
const deleteBtn = screen.getByTestId("btn-delete-type-to-confirm-delete");
61+
expect(deleteBtn).toBeDisabled();
62+
});
63+
64+
it("Delete button is enabled when input matches deployment name exactly", () => {
65+
renderDialog(defaultProps);
66+
67+
const input = screen.getByTestId("input-type-to-confirm-delete");
68+
fireEvent.change(input, { target: { value: "My Agent" } });
69+
70+
const deleteBtn = screen.getByTestId("btn-delete-type-to-confirm-delete");
71+
expect(deleteBtn).toBeEnabled();
72+
});
73+
74+
it("calls onConfirm when Delete button is clicked with correct input", () => {
75+
const onConfirm = jest.fn();
76+
renderDialog({ ...defaultProps, onConfirm });
77+
78+
const input = screen.getByTestId("input-type-to-confirm-delete");
79+
fireEvent.change(input, { target: { value: "My Agent" } });
80+
81+
const deleteBtn = screen.getByTestId("btn-delete-type-to-confirm-delete");
82+
fireEvent.click(deleteBtn);
83+
84+
expect(onConfirm).toHaveBeenCalledTimes(1);
85+
});
86+
87+
it("Delete button is disabled when input matches deployment name with different casing", () => {
88+
renderDialog(defaultProps);
89+
90+
const input = screen.getByTestId("input-type-to-confirm-delete");
91+
fireEvent.change(input, { target: { value: "my agent" } });
92+
93+
const deleteBtn = screen.getByTestId("btn-delete-type-to-confirm-delete");
94+
expect(deleteBtn).toBeDisabled();
95+
});
96+
97+
it("input resets to empty when dialog is closed (open becomes false)", () => {
98+
const { rerender } = renderDialog({ ...defaultProps, open: true });
99+
100+
const input = screen.getByTestId("input-type-to-confirm-delete");
101+
fireEvent.change(input, { target: { value: "My Agent" } });
102+
expect(input).toHaveValue("My Agent");
103+
104+
rerenderDialog(rerender, { ...defaultProps, open: false });
105+
106+
// Input should be reset; since dialog is closed, query the component's
107+
// internal state by reopening it
108+
rerenderDialog(rerender, { ...defaultProps, open: true });
109+
110+
const resetInput = screen.getByTestId("input-type-to-confirm-delete");
111+
expect(resetInput).toHaveValue("");
112+
});
113+
});

src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployments-content.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from "@/components/ui/select";
99
import { useDeleteDeployment } from "@/controllers/API/queries/deployments/use-delete-deployment";
1010
import { useGetDeploymentsByProviders } from "@/controllers/API/queries/deployments/use-get-deployments-by-providers";
11-
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
1211
import { useDeleteWithConfirmation } from "../hooks/use-delete-with-confirmation";
1312
import { ALL_PROVIDERS, useProviderFilter } from "../hooks/use-provider-filter";
1413
import { useTestDeploymentModal } from "../hooks/use-test-deployment-modal";
@@ -19,6 +18,7 @@ import DeploymentsEmptyState from "./deployments-empty-state";
1918
import DeploymentsLoadingSkeleton from "./deployments-loading-skeleton";
2019
import DeploymentsTable from "./deployments-table";
2120
import TestDeploymentModal from "./test-deployment-modal/test-deployment-modal";
21+
import TypeToConfirmDeleteDialog from "./type-to-confirm-delete-dialog";
2222

2323
const buildDeploymentDeleteParams = (id: string) => ({ deployment_id: id });
2424

@@ -49,11 +49,10 @@ export default function DeploymentsContent({
4949

5050
const { mutate: deleteDeployment } = useDeleteDeployment();
5151

52-
const deploymentDelete = useDeleteWithConfirmation(
53-
deleteDeployment,
54-
buildDeploymentDeleteParams,
55-
"Error deleting deployment",
56-
);
52+
const deploymentDelete = useDeleteWithConfirmation<
53+
Deployment,
54+
{ deployment_id: string }
55+
>(deleteDeployment, buildDeploymentDeleteParams, "Error deleting deployment");
5756

5857
const [editingDeployment, setEditingDeployment] = useState<Deployment | null>(
5958
null,
@@ -154,10 +153,10 @@ export default function DeploymentsContent({
154153
}
155154
/>
156155

157-
<DeleteConfirmationModal
156+
<TypeToConfirmDeleteDialog
158157
open={!!deploymentDelete.target}
159-
setOpen={deploymentDelete.setModalOpen}
160-
description={`deployment "${deploymentDelete.target?.name}"`}
158+
onOpenChange={deploymentDelete.setModalOpen}
159+
deploymentName={deploymentDelete.target?.name ?? ""}
161160
onConfirm={deploymentDelete.confirmDelete}
162161
/>
163162
</>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { DialogClose } from "@radix-ui/react-dialog";
2+
import { AlertTriangle } from "lucide-react";
3+
import { useEffect, useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from "@/components/ui/dialog";
12+
import { Input } from "@/components/ui/input";
13+
14+
interface TypeToConfirmDeleteDialogProps {
15+
open: boolean;
16+
onOpenChange: (open: boolean) => void;
17+
deploymentName: string;
18+
onConfirm: (e: React.MouseEvent<HTMLButtonElement>) => void;
19+
}
20+
21+
export default function TypeToConfirmDeleteDialog({
22+
open,
23+
onOpenChange,
24+
deploymentName,
25+
onConfirm,
26+
}: TypeToConfirmDeleteDialogProps) {
27+
const [inputValue, setInputValue] = useState("");
28+
29+
useEffect(() => {
30+
if (!open) {
31+
setInputValue("");
32+
}
33+
}, [open]);
34+
35+
const isConfirmDisabled = inputValue !== deploymentName;
36+
37+
return (
38+
<Dialog open={open} onOpenChange={onOpenChange}>
39+
<DialogContent>
40+
<DialogHeader>
41+
<DialogTitle>
42+
<div className="flex items-center">
43+
<AlertTriangle
44+
className="mr-2 h-6 w-6 text-destructive"
45+
strokeWidth={1.5}
46+
/>
47+
<span className="pl-2">Delete</span>
48+
</div>
49+
</DialogTitle>
50+
</DialogHeader>
51+
<div className="flex flex-col gap-3 pb-3 text-sm">
52+
<p>
53+
Permanently delete the deployment <strong>{deploymentName}</strong>{" "}
54+
in Langflow and Watsonx Orchestrate.
55+
</p>
56+
<label htmlFor="confirm-delete-input" className="text-sm">
57+
Type the deployment name to confirm:{" "}
58+
<code className="font-mono bg-muted px-1 rounded text-sm">
59+
{deploymentName}
60+
</code>
61+
</label>
62+
<Input
63+
id="confirm-delete-input"
64+
autoFocus
65+
placeholder={deploymentName}
66+
value={inputValue}
67+
onChange={(e) => setInputValue(e.target.value)}
68+
data-testid="input-type-to-confirm-delete"
69+
/>
70+
<p>This can't be undone.</p>
71+
</div>
72+
<DialogFooter>
73+
<DialogClose asChild>
74+
<Button
75+
onClick={(e) => e.stopPropagation()}
76+
className="mr-1"
77+
variant="outline"
78+
data-testid="btn-cancel-type-to-confirm-delete"
79+
>
80+
Cancel
81+
</Button>
82+
</DialogClose>
83+
<Button
84+
type="submit"
85+
variant="destructive"
86+
disabled={isConfirmDisabled}
87+
onClick={onConfirm}
88+
data-testid="btn-delete-type-to-confirm-delete"
89+
>
90+
Delete
91+
</Button>
92+
</DialogFooter>
93+
</DialogContent>
94+
</Dialog>
95+
);
96+
}

0 commit comments

Comments
 (0)