feat: require typed confirmation for license removal (#22082)

## Summary
Adds a typed-confirmation step before deleting a deployment license to
reduce accidental removals.

<img width="457" height="440" alt="Screenshot 2026-02-13 at 15 31 58"
src="https://github.com/user-attachments/assets/b13320a7-4b10-43fa-ab01-56f3284435b6"
/>

## Changes
- Swapped the license removal dialog from `ConfirmDialog` to
`DeleteDialog`, requiring the admin to type the license ID before
enabling **Remove**.
- Added interaction coverage to verify the confirmation guard.
This commit is contained in:
Jaayden Halko
2026-02-28 07:58:10 +00:00
committed by GitHub
parent 60793aa277
commit f95ae63c96
2 changed files with 53 additions and 14 deletions
@@ -1,6 +1,6 @@
import { MockLicenseResponse } from "testHelpers/entities";
import { render } from "testHelpers/renderHelpers";
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LicenseCard } from "./LicenseCard";
@@ -56,7 +56,8 @@ describe("LicenseCard", () => {
const removeButton = await screen.findByRole("button", { name: /remove/i });
await user.click(removeButton);
await screen.findByText(/This license has already expired/);
const dialog = await screen.findByTestId("dialog");
expect(dialog).toHaveTextContent(/This license has already expired/);
});
it("shows disabling features warning for active licenses", async () => {
@@ -106,4 +107,40 @@ describe("LicenseCard", () => {
// Then
await screen.findByText("1 / 3");
});
it("requires typing the license ID before allowing removal", async () => {
const user = userEvent.setup();
const onRemove = vi.fn();
const license = MockLicenseResponse[0];
render(
<LicenseCard
license={license}
userLimitActual={1}
userLimitLimit={10}
onRemove={onRemove}
isRemoving={false}
/>,
);
await user.click(screen.getByRole("button", { name: /remove/i }));
const dialog = await screen.findByTestId("dialog");
const dialogScope = within(dialog);
const confirmButton = dialogScope.getByRole("button", { name: "Remove" });
expect(confirmButton).toBeDisabled();
const confirmationInput = dialogScope.getByTestId(
"delete-dialog-name-confirmation",
);
await user.type(confirmationInput, "wrong");
expect(confirmButton).toBeDisabled();
await user.clear(confirmationInput);
await user.type(confirmationInput, String(license.id));
expect(confirmButton).toBeEnabled();
await user.click(confirmButton);
expect(onRemove).toHaveBeenCalledWith(license.id);
});
});
@@ -1,6 +1,6 @@
import type { GetLicensesResponse } from "api/api";
import { Button } from "components/Button/Button";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { Pill } from "components/Pill/Pill";
import dayjs from "dayjs";
import { type FC, useState } from "react";
@@ -25,6 +25,7 @@ export const LicenseCard: FC<LicenseCardProps> = ({
>(undefined);
const currentUserLimit = license.claims.features.user_limit || userLimitLimit;
const confirmationName = licenseIDMarkedForRemoval?.toString() ?? "";
const isExpired = dayjs
.unix(license.claims.license_expires)
@@ -41,26 +42,27 @@ export const LicenseCard: FC<LicenseCardProps> = ({
key={license.id}
className="license-card rounded-lg border border-solid border-border bg-surface-secondary p-4 text-sm shadow-sm"
>
<ConfirmDialog
type="delete"
hideCancel={false}
open={licenseIDMarkedForRemoval !== undefined}
<DeleteDialog
key={licenseIDMarkedForRemoval}
isOpen={licenseIDMarkedForRemoval !== undefined}
onConfirm={() => {
if (!licenseIDMarkedForRemoval) {
return;
}
if (!licenseIDMarkedForRemoval) return;
onRemove(licenseIDMarkedForRemoval);
setLicenseIDMarkedForRemoval(undefined);
}}
onClose={() => setLicenseIDMarkedForRemoval(undefined)}
title="Confirm License Removal"
confirmLoading={isRemoving}
onCancel={() => setLicenseIDMarkedForRemoval(undefined)}
entity="license"
name={confirmationName}
label="ID of the license to remove"
title="Confirm license removal"
verb="Removing"
confirmText="Remove"
description={
info={
isExpired
? "This license has already expired and is not providing any features. Removing it will not affect your current entitlements."
: "Removing this license will disable all Premium features. You can add a new license at any time."
}
confirmLoading={isRemoving}
/>
<div className="flex flex-row gap-4 items-center">
<span className="text-content-secondary text-lg font-semibold">