fix(frontend): add force-delete flow and try/catch for credential operations

- DeleteConfirmationModal now shows backend warning message and offers
  "Force Delete" when API returns need_confirmation instead of just a
  toast (mirrors integrations page pattern)
- HostScopedCredentialsModal onSubmit delete-then-create is now wrapped
  in try/catch to prevent silent credential loss on creation failure
This commit is contained in:
Zamil Majdy
2026-04-02 17:25:04 +02:00
parent eeba884671
commit b256560619
4 changed files with 68 additions and 32 deletions

View File

@@ -94,6 +94,7 @@ export function CredentialsInput({
handleDeleteCredential,
handleDeleteConfirm,
credentialToDelete,
deleteWarningMessage,
setCredentialToDelete,
isDeletingCredential,
} = hookData;
@@ -202,9 +203,11 @@ export function CredentialsInput({
<DeleteConfirmationModal
credentialToDelete={credentialToDelete}
warningMessage={deleteWarningMessage}
isDeleting={isDeletingCredential}
onClose={() => setCredentialToDelete(null)}
onConfirm={handleDeleteConfirm}
onForceConfirm={() => handleDeleteConfirm(true)}
/>
</>
)}

View File

@@ -4,16 +4,20 @@ import { Dialog } from "@/components/molecules/Dialog/Dialog";
interface Props {
credentialToDelete: { id: string; title: string } | null;
warningMessage?: string | null;
isDeleting: boolean;
onClose: () => void;
onConfirm: () => void;
onForceConfirm: () => void;
}
export function DeleteConfirmationModal({
credentialToDelete,
warningMessage,
isDeleting,
onClose,
onConfirm,
onForceConfirm,
}: Props) {
return (
<Dialog
@@ -27,21 +31,35 @@ export function DeleteConfirmationModal({
styling={{ maxWidth: "32rem" }}
>
<Dialog.Content>
<Text variant="large">
Are you sure you want to delete &quot;{credentialToDelete?.title}
&quot;? This action cannot be undone.
</Text>
{warningMessage ? (
<Text variant="large">{warningMessage}</Text>
) : (
<Text variant="large">
Are you sure you want to delete &quot;{credentialToDelete?.title}
&quot;? This action cannot be undone.
</Text>
)}
<Dialog.Footer>
<Button variant="secondary" onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirm}
loading={isDeleting}
>
Delete
</Button>
{warningMessage ? (
<Button
variant="destructive"
onClick={onForceConfirm}
loading={isDeleting}
>
Force Delete
</Button>
) : (
<Button
variant="destructive"
onClick={onConfirm}
loading={isDeleting}
>
Delete
</Button>
)}
</Dialog.Footer>
</Dialog.Content>
</Dialog>

View File

@@ -19,6 +19,7 @@ import {
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { getHostFromUrl } from "@/lib/utils/url";
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { toast } from "@/components/molecules/Toast/use-toast";
type Props = {
schema: BlockIOCredentialsSubSchema;
@@ -149,22 +150,33 @@ export function HostScopedCredentialsModal({
const existingForHost = allProviderCredentials.filter(
(c) => c.type === "host_scoped" && "host" in c && c.host === host,
);
for (const existing of existingForHost) {
await deleteCredentials(existing.id, true);
try {
for (const existing of existingForHost) {
await deleteCredentials(existing.id, true);
}
const newCredentials = await createHostScopedCredentials({
host,
title: currentHost || host,
headers,
});
onCredentialsCreate({
provider,
id: newCredentials.id,
type: "host_scoped",
title: newCredentials.title,
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Something went wrong";
toast({
title: "Failed to save credentials",
description: message,
variant: "destructive",
});
}
const newCredentials = await createHostScopedCredentials({
host,
title: currentHost || host,
headers,
});
onCredentialsCreate({
provider,
id: newCredentials.id,
type: "host_scoped",
title: newCredentials.title,
});
}
return (

View File

@@ -58,6 +58,9 @@ export function useCredentialsInput({
id: string;
title: string;
} | null>(null);
const [deleteWarningMessage, setDeleteWarningMessage] = useState<
string | null
>(null);
const api = useBackendAPI();
const credentials = useCredentials(schema, siblingInputs);
@@ -297,6 +300,7 @@ export function useCredentialsInput({
}
function handleDeleteCredential(credential: { id: string; title: string }) {
setDeleteWarningMessage(null);
setCredentialToDelete(credential);
}
@@ -319,14 +323,12 @@ export function useCredentialsInput({
if (selectedCredential?.id === credentialToDelete.id) {
onSelectCredential(undefined);
}
setDeleteWarningMessage(null);
setCredentialToDelete(null);
} else if ("need_confirmation" in result && result.need_confirmation) {
toast({
title: "Credential is in use",
description: result.message,
variant: "destructive",
duration: 8000,
});
setDeleteWarningMessage(
result.message || "This credential is in use. Force delete?",
);
}
} catch (error) {
const message =
@@ -364,6 +366,7 @@ export function useCredentialsInput({
isOAuth2FlowInProgress,
cancelOAuthFlow,
credentialToDelete,
deleteWarningMessage,
isDeletingCredential,
actionButtonText: getActionButtonText(
supportsOAuth2,