From f95ae63c96ec01c9f7645426d58b9b74565ac5f2 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Sat, 28 Feb 2026 14:58:10 +0700 Subject: [PATCH] feat: require typed confirmation for license removal (#22082) ## Summary Adds a typed-confirmation step before deleting a deployment license to reduce accidental removals. Screenshot 2026-02-13 at 15 31 58 ## 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. --- .../LicensesSettingsPage/LicenseCard.test.tsx | 41 ++++++++++++++++++- .../LicensesSettingsPage/LicenseCard.tsx | 26 ++++++------ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.test.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.test.tsx index 09737c1e98..294ec59a41 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.test.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.test.tsx @@ -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( + , + ); + + 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); + }); }); diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx index 9f93de7368..169fe25328 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicenseCard.tsx @@ -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 = ({ >(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 = ({ key={license.id} className="license-card rounded-lg border border-solid border-border bg-surface-secondary p-4 text-sm shadow-sm" > - { - 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} />