fix(frontend): replace duplicate host-scoped credentials and add delete support

- HostScopedCredentialsModal now deletes existing credentials for the same
  host before creating new ones, preventing duplicates
- Wire up delete flow: CredentialsFlatView passes onDelete to CredentialRow,
  CredentialsInput renders DeleteConfirmationModal
- Update button text to "Update headers" when credentials already exist
- Dynamic modal title/button: "Update" vs "Add" based on existing creds
This commit is contained in:
Zamil Majdy
2026-04-02 07:51:59 +02:00
parent c4ff31c79c
commit beb43bb847
4 changed files with 59 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ import { toDisplayName } from "@/providers/agent-credentials/helper";
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
import { CredentialsFlatView } from "./components/CredentialsFlatView/CredentialsFlatView";
import { CredentialTypeSelector } from "./components/CredentialTypeSelector/CredentialTypeSelector";
import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal/DeleteConfirmationModal";
import { HostScopedCredentialsModal } from "./components/HotScopedCredentialsModal/HotScopedCredentialsModal";
import { OAuthFlowWaitingModal } from "./components/OAuthWaitingModal/OAuthWaitingModal";
import { PasswordCredentialsModal } from "./components/PasswordCredentialsModal/PasswordCredentialsModal";
@@ -90,6 +91,11 @@ export function CredentialsInput({
handleActionButtonClick,
handleCredentialSelect,
handleOAuthLogin,
handleDeleteCredential,
handleDeleteConfirm,
credentialToDelete,
setCredentialToDelete,
deleteCredentialsMutation,
} = hookData;
const displayName = toDisplayName(provider);
@@ -113,6 +119,7 @@ export function CredentialsInput({
onSelectCredential={handleCredentialSelect}
onClearCredential={() => onSelectCredential(undefined)}
onAddCredential={handleActionButtonClick}
onDeleteCredential={readOnly ? undefined : handleDeleteCredential}
actionButtonText={actionButtonText}
isOptional={isOptional}
showTitle={showTitle}
@@ -192,6 +199,13 @@ export function CredentialsInput({
Error: {oAuthError}
</Text>
)}
<DeleteConfirmationModal
credentialToDelete={credentialToDelete}
isDeleting={deleteCredentialsMutation.isPending}
onClose={() => setCredentialToDelete(null)}
onConfirm={handleDeleteConfirm}
/>
</>
)}
</div>

View File

@@ -31,6 +31,7 @@ type Props = {
onSelectCredential: (credentialId: string) => void;
onClearCredential: () => void;
onAddCredential: () => void;
onDeleteCredential?: (credential: { id: string; title: string }) => void;
};
export function CredentialsFlatView({
@@ -47,6 +48,7 @@ export function CredentialsFlatView({
onSelectCredential,
onClearCredential,
onAddCredential,
onDeleteCredential,
}: Props) {
const hasCredentials = credentials.length > 0;
@@ -99,6 +101,15 @@ export function CredentialsFlatView({
provider={provider}
displayName={displayName}
onSelect={() => onSelectCredential(credential.id)}
onDelete={
onDeleteCredential
? () =>
onDeleteCredential({
id: credential.id,
title: credential.title || credential.id,
})
: undefined
}
readOnly={readOnly}
/>
))}

View File

@@ -89,7 +89,20 @@ export function HostScopedCredentialsModal({
return null;
}
const { provider, providerName, createHostScopedCredentials } = credentials;
const {
provider,
providerName,
savedCredentials,
createHostScopedCredentials,
deleteCredentials,
} = credentials;
const hasExistingForHost = savedCredentials.some(
(c) =>
c.type === "host_scoped" &&
"host" in c &&
c.host === (currentHost || form.getValues("host")),
);
const addHeaderPair = () => {
setHeaderPairs([...headerPairs, { key: "", value: "" }]);
@@ -123,9 +136,18 @@ export function HostScopedCredentialsModal({
{} as Record<string, string>,
);
// Delete existing host-scoped credentials for the same host to avoid duplicates
const host = values.host;
const existingForHost = savedCredentials.filter(
(c) => c.type === "host_scoped" && "host" in c && c.host === host,
);
for (const existing of existingForHost) {
await deleteCredentials(existing.id, true);
}
const newCredentials = await createHostScopedCredentials({
host: values.host,
title: currentHost || values.host,
host,
title: currentHost || host,
headers,
});
@@ -139,7 +161,11 @@ export function HostScopedCredentialsModal({
return (
<Dialog
title={`Add sensitive headers for ${providerName}`}
title={
hasExistingForHost
? `Update sensitive headers for ${providerName}`
: `Add sensitive headers for ${providerName}`
}
controlled={{
isOpen: open,
set: (isOpen) => {
@@ -241,7 +267,9 @@ export function HostScopedCredentialsModal({
<div className="pt-8">
<Button type="submit" className="w-full" size="small">
Save & use these credentials
{hasExistingForHost
? "Update & use these credentials"
: "Save & use these credentials"}
</Button>
</div>
</form>

View File

@@ -148,7 +148,7 @@ export function getActionButtonText(
if (supportsOAuth2) return "Connect another account";
if (supportsApiKey) return "Use a new API key";
if (supportsUserPassword) return "Add a new username and password";
if (supportsHostScoped) return "Add new headers";
if (supportsHostScoped) return "Update headers";
return "Add new credentials";
} else {
if (supportsOAuth2) return "Add account";