mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-16 17:55:55 -05:00
## Changes 🏗️ On the **Old Builder**, when running an agent... ### Before <img width="800" height="614" alt="Screenshot 2026-01-21 at 21 27 05" src="https://github.com/user-attachments/assets/a3b2ec17-597f-44d2-9130-9e7931599c38" /> Credentials are there, but it is not recognising them, you need to click on them to be selected ### After <img width="1029" height="728" alt="Screenshot 2026-01-21 at 21 26 47" src="https://github.com/user-attachments/assets/c6e83846-6048-439e-919d-6807674f2d5a" /> It uses the new credentials UI and correctly auto-selects existing ones. ### Other Fixed a small timezone display glitch on the new library view. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run agent in old builder - [x] Credentials are auto-selected and using the new collapsed system credentials UI
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import { useDeleteV1DeleteCredentials } from "@/app/api/__generated__/endpoints/integrations/integrations";
|
|
import useCredentials from "@/hooks/useCredentials";
|
|
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
|
import {
|
|
BlockIOCredentialsSubSchema,
|
|
CredentialsMetaInput,
|
|
} from "@/lib/autogpt-server-api/types";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
filterSystemCredentials,
|
|
getActionButtonText,
|
|
getSystemCredentials,
|
|
OAUTH_TIMEOUT_MS,
|
|
OAuthPopupResultMessage,
|
|
} from "./helpers";
|
|
|
|
export type CredentialsInputState = ReturnType<typeof useCredentialsInput>;
|
|
|
|
type Params = {
|
|
schema: BlockIOCredentialsSubSchema;
|
|
selectedCredential?: CredentialsMetaInput;
|
|
onSelectCredential: (newValue?: CredentialsMetaInput) => void;
|
|
siblingInputs?: Record<string, any>;
|
|
onLoaded?: (loaded: boolean) => void;
|
|
readOnly?: boolean;
|
|
isOptional?: boolean;
|
|
};
|
|
|
|
export function useCredentialsInput({
|
|
schema,
|
|
selectedCredential,
|
|
onSelectCredential,
|
|
siblingInputs,
|
|
onLoaded,
|
|
readOnly = false,
|
|
isOptional = false,
|
|
}: Params) {
|
|
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
|
useState(false);
|
|
const [
|
|
isUserPasswordCredentialsModalOpen,
|
|
setUserPasswordCredentialsModalOpen,
|
|
] = useState(false);
|
|
const [isHostScopedCredentialsModalOpen, setHostScopedCredentialsModalOpen] =
|
|
useState(false);
|
|
const [isOAuth2FlowInProgress, setOAuth2FlowInProgress] = useState(false);
|
|
const [oAuthPopupController, setOAuthPopupController] =
|
|
useState<AbortController | null>(null);
|
|
const [oAuthError, setOAuthError] = useState<string | null>(null);
|
|
const [credentialToDelete, setCredentialToDelete] = useState<{
|
|
id: string;
|
|
title: string;
|
|
} | null>(null);
|
|
|
|
const api = useBackendAPI();
|
|
const queryClient = useQueryClient();
|
|
const credentials = useCredentials(schema, siblingInputs);
|
|
const hasAttemptedAutoSelect = useRef(false);
|
|
|
|
const deleteCredentialsMutation = useDeleteV1DeleteCredentials({
|
|
mutation: {
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["/api/integrations/credentials"],
|
|
});
|
|
queryClient.invalidateQueries({
|
|
queryKey: [`/api/integrations/${credentials?.provider}/credentials`],
|
|
});
|
|
setCredentialToDelete(null);
|
|
if (selectedCredential?.id === credentialToDelete?.id) {
|
|
onSelectCredential(undefined);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (onLoaded) {
|
|
onLoaded(Boolean(credentials && credentials.isLoading === false));
|
|
}
|
|
}, [credentials, onLoaded]);
|
|
|
|
// Unselect credential if not available
|
|
useEffect(() => {
|
|
if (readOnly) return;
|
|
if (!credentials || !("savedCredentials" in credentials)) return;
|
|
const availableCreds = credentials.savedCredentials;
|
|
if (
|
|
selectedCredential &&
|
|
!availableCreds.some((c) => c.id === selectedCredential.id)
|
|
) {
|
|
onSelectCredential(undefined);
|
|
// Reset auto-selection flag so it can run again after unsetting invalid credential
|
|
hasAttemptedAutoSelect.current = false;
|
|
}
|
|
}, [credentials, selectedCredential, onSelectCredential, readOnly]);
|
|
|
|
// Auto-select the first available credential on initial mount
|
|
// Once a user has made a selection, we don't override it
|
|
useEffect(
|
|
function autoSelectCredential() {
|
|
if (readOnly) return;
|
|
if (!credentials || !("savedCredentials" in credentials)) return;
|
|
if (selectedCredential?.id) return;
|
|
|
|
const savedCreds = credentials.savedCredentials;
|
|
if (savedCreds.length === 0) return;
|
|
|
|
if (hasAttemptedAutoSelect.current) return;
|
|
hasAttemptedAutoSelect.current = true;
|
|
|
|
if (isOptional) return;
|
|
|
|
const cred = savedCreds[0];
|
|
onSelectCredential({
|
|
id: cred.id,
|
|
type: cred.type,
|
|
provider: credentials.provider,
|
|
title: (cred as any).title,
|
|
});
|
|
},
|
|
[
|
|
credentials,
|
|
selectedCredential?.id,
|
|
readOnly,
|
|
isOptional,
|
|
onSelectCredential,
|
|
],
|
|
);
|
|
|
|
if (
|
|
!credentials ||
|
|
credentials.isLoading ||
|
|
!("savedCredentials" in credentials)
|
|
) {
|
|
return {
|
|
isLoading: true,
|
|
};
|
|
}
|
|
|
|
const {
|
|
provider,
|
|
providerName,
|
|
supportsApiKey,
|
|
supportsOAuth2,
|
|
supportsUserPassword,
|
|
supportsHostScoped,
|
|
savedCredentials,
|
|
oAuthCallback,
|
|
isSystemProvider,
|
|
} = credentials;
|
|
|
|
// Split credentials into user and system
|
|
const userCredentials = filterSystemCredentials(savedCredentials);
|
|
const systemCredentials = getSystemCredentials(savedCredentials);
|
|
|
|
async function handleOAuthLogin() {
|
|
setOAuthError(null);
|
|
const { login_url, state_token } = await api.oAuthLogin(
|
|
provider,
|
|
schema.credentials_scopes,
|
|
);
|
|
setOAuth2FlowInProgress(true);
|
|
const popup = window.open(login_url, "_blank", "popup=true");
|
|
|
|
if (!popup) {
|
|
throw new Error(
|
|
"Failed to open popup window. Please allow popups for this site.",
|
|
);
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
setOAuthPopupController(controller);
|
|
controller.signal.onabort = () => {
|
|
console.debug("OAuth flow aborted");
|
|
setOAuth2FlowInProgress(false);
|
|
popup.close();
|
|
};
|
|
|
|
const handleMessage = async (e: MessageEvent<OAuthPopupResultMessage>) => {
|
|
console.debug("Message received:", e.data);
|
|
if (
|
|
typeof e.data != "object" ||
|
|
!("message_type" in e.data) ||
|
|
e.data.message_type !== "oauth_popup_result"
|
|
) {
|
|
console.debug("Ignoring irrelevant message");
|
|
return;
|
|
}
|
|
|
|
if (!e.data.success) {
|
|
console.error("OAuth flow failed:", e.data.message);
|
|
setOAuthError(`OAuth flow failed: ${e.data.message}`);
|
|
setOAuth2FlowInProgress(false);
|
|
return;
|
|
}
|
|
|
|
if (e.data.state !== state_token) {
|
|
console.error("Invalid state token received");
|
|
setOAuthError("Invalid state token received");
|
|
setOAuth2FlowInProgress(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.debug("Processing OAuth callback");
|
|
const credentials = await oAuthCallback(e.data.code, e.data.state);
|
|
console.debug("OAuth callback processed successfully");
|
|
|
|
// Check if the credential's scopes match the required scopes
|
|
const requiredScopes = schema.credentials_scopes;
|
|
if (requiredScopes && requiredScopes.length > 0) {
|
|
const grantedScopes = new Set(credentials.scopes || []);
|
|
const hasAllRequiredScopes = new Set(requiredScopes).isSubsetOf(
|
|
grantedScopes,
|
|
);
|
|
|
|
if (!hasAllRequiredScopes) {
|
|
console.error(
|
|
`Newly created OAuth credential for ${providerName} has insufficient scopes. Required:`,
|
|
requiredScopes,
|
|
"Granted:",
|
|
credentials.scopes,
|
|
);
|
|
setOAuthError(
|
|
"Connection failed: the granted permissions don't match what's required. " +
|
|
"Please contact the application administrator.",
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
onSelectCredential({
|
|
id: credentials.id,
|
|
type: "oauth2",
|
|
title: credentials.title,
|
|
provider,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in OAuth callback:", error);
|
|
setOAuthError(
|
|
`Error in OAuth callback: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
);
|
|
} finally {
|
|
console.debug("Finalizing OAuth flow");
|
|
setOAuth2FlowInProgress(false);
|
|
controller.abort("success");
|
|
}
|
|
};
|
|
|
|
console.debug("Adding message event listener");
|
|
window.addEventListener("message", handleMessage, {
|
|
signal: controller.signal,
|
|
});
|
|
|
|
setTimeout(() => {
|
|
console.debug("OAuth flow timed out");
|
|
controller.abort("timeout");
|
|
setOAuth2FlowInProgress(false);
|
|
setOAuthError("OAuth flow timed out");
|
|
}, OAUTH_TIMEOUT_MS);
|
|
}
|
|
|
|
function handleActionButtonClick() {
|
|
if (supportsOAuth2) {
|
|
handleOAuthLogin();
|
|
} else if (supportsApiKey) {
|
|
setAPICredentialsModalOpen(true);
|
|
} else if (supportsUserPassword) {
|
|
setUserPasswordCredentialsModalOpen(true);
|
|
} else if (supportsHostScoped) {
|
|
setHostScopedCredentialsModalOpen(true);
|
|
}
|
|
}
|
|
|
|
function handleCredentialSelect(credentialId: string) {
|
|
const selectedCreds = savedCredentials.find((c) => c.id === credentialId);
|
|
if (selectedCreds) {
|
|
onSelectCredential({
|
|
id: selectedCreds.id,
|
|
type: selectedCreds.type,
|
|
provider: provider,
|
|
title: (selectedCreds as any).title,
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleDeleteCredential(credential: { id: string; title: string }) {
|
|
setCredentialToDelete(credential);
|
|
}
|
|
|
|
function handleDeleteConfirm() {
|
|
if (credentialToDelete && credentials) {
|
|
deleteCredentialsMutation.mutate({
|
|
provider: credentials.provider,
|
|
credId: credentialToDelete.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
isLoading: false as const,
|
|
provider,
|
|
providerName,
|
|
supportsApiKey,
|
|
supportsOAuth2,
|
|
supportsUserPassword,
|
|
supportsHostScoped,
|
|
isSystemProvider,
|
|
userCredentials,
|
|
systemCredentials,
|
|
allCredentials: savedCredentials,
|
|
selectedCredential,
|
|
oAuthError,
|
|
isAPICredentialsModalOpen,
|
|
isUserPasswordCredentialsModalOpen,
|
|
isHostScopedCredentialsModalOpen,
|
|
isOAuth2FlowInProgress,
|
|
oAuthPopupController,
|
|
credentialToDelete,
|
|
deleteCredentialsMutation,
|
|
actionButtonText: getActionButtonText(
|
|
supportsOAuth2,
|
|
supportsApiKey,
|
|
supportsUserPassword,
|
|
supportsHostScoped,
|
|
userCredentials.length > 0,
|
|
),
|
|
setAPICredentialsModalOpen,
|
|
setUserPasswordCredentialsModalOpen,
|
|
setHostScopedCredentialsModalOpen,
|
|
setCredentialToDelete,
|
|
handleActionButtonClick,
|
|
handleCredentialSelect,
|
|
handleDeleteCredential,
|
|
handleDeleteConfirm,
|
|
handleOAuthLogin,
|
|
onSelectCredential,
|
|
schema,
|
|
siblingInputs,
|
|
};
|
|
}
|