mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(frontend): add new credential field for new builder (#11066)
In this PR, I’ve added a feature to select a credential from a list and also provided a UI to create a new credential if desired. <img width="443" height="157" alt="Screenshot 2025-10-06 at 9 28 07 AM" src="https://github.com/user-attachments/assets/d9e72a14-255d-45b6-aa61-b55c2465dd7e" /> #### Frontend Changes: - **Refactored credential field** from a single component to a modular architecture: - Created `CredentialField/` directory with separated concerns - Added `SelectCredential.tsx` component for credential selection UI with provider details display - Implemented `useCredentialField.ts` custom hook for credential data fetching with 10-minute caching - Added `helpers.ts` with credential filtering and provider name formatting utilities - Added loading states with skeleton UI while fetching credentials - **Enhanced UI/UX features**: - Dropdown selector showing credentials with provider, title, username, and host details - Visual key icon for each credential option - Placeholder "Add API Key" button (implementation pending) - Loading skeleton UI for better perceived performance - Smart filtering of credentials based on provider requirements - **Template improvements**: - Updated `FieldTemplate.tsx` to properly handle credential field display - Special handling for credential field labels showing provider-specific names - Removed input handle for credential fields in the node editor #### Backend Changes: - **API Documentation improvements**: - Added OpenAPI summaries to `/credentials` endpoint ("List Credentials") - Added summary to `/{provider}/credentials/{cred_id}` endpoint ("Get Specific Credential By ID") ### Test Plan 📋 - [x] Navigate to the flow builder - [x] Add a block that requires credentials (e.g., API block) - [x] Verify the credential dropdown loads and displays available credentials - [x] Check that only credentials matching the provider requirements are shown
This commit is contained in:
@@ -180,7 +180,7 @@ async def callback(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/credentials")
|
||||
@router.get("/credentials", summary="List Credentials")
|
||||
async def list_credentials(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> list[CredentialsMetaResponse]:
|
||||
@@ -221,7 +221,9 @@ async def list_credentials_by_provider(
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{provider}/credentials/{cred_id}")
|
||||
@router.get(
|
||||
"/{provider}/credentials/{cred_id}", summary="Get Specific Credential By ID"
|
||||
)
|
||||
async def get_credential(
|
||||
provider: Annotated[
|
||||
ProviderName, Path(title="The provider to retrieve credentials for")
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
|
||||
// We need to add all the logic for the credential fields here
|
||||
export const CredentialsField = (props: FieldProps) => {
|
||||
const { formData = {}, onChange, required: _required, schema } = props;
|
||||
|
||||
const _credentialProvider = schema.credentials_provider;
|
||||
const _credentialType = schema.credentials_types;
|
||||
const _description = schema.description;
|
||||
const _title = schema.title;
|
||||
|
||||
// Helper to update one property
|
||||
const setField = (key: string, value: any) =>
|
||||
onChange({ ...formData, [key]: value });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
hideLabel={true}
|
||||
label={""}
|
||||
id="credentials-id"
|
||||
type="text"
|
||||
value={formData.id || ""}
|
||||
onChange={(e) => setField("id", e.target.value)}
|
||||
placeholder="Enter your API Key"
|
||||
required
|
||||
size="small"
|
||||
wrapperClassName="mb-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { FieldProps } from "@rjsf/utils";
|
||||
import { useCredentialField } from "./useCredentialField";
|
||||
import { filterCredentialsByProvider } from "./helpers";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { SelectCredential } from "./SelectCredential";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
|
||||
export const CredentialsField = (props: FieldProps) => {
|
||||
const { formData = {}, onChange, required: _required, schema } = props;
|
||||
const { credentials, isCredentialListLoading } = useCredentialField();
|
||||
|
||||
const credentialProviders = schema.credentials_provider;
|
||||
const { credentials: filteredCredentials, exists: credentialsExists } =
|
||||
filterCredentialsByProvider(credentials, credentialProviders);
|
||||
|
||||
const setField = (key: string, value: any) =>
|
||||
onChange({ ...formData, [key]: value });
|
||||
|
||||
if (isCredentialListLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-8 w-full rounded-xlarge" />
|
||||
<Skeleton className="h-8 w-[30%] rounded-xlarge" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{credentialsExists && (
|
||||
<SelectCredential
|
||||
credentials={filteredCredentials}
|
||||
value={formData.id}
|
||||
onChange={(value) => setField("id", value)}
|
||||
disabled={false}
|
||||
label="Credential"
|
||||
placeholder="Select credential"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TODO : We need to add a modal to add a new credential */}
|
||||
<Button type="button" className="w-fit" size="small">
|
||||
<PlusIcon /> Add API Key
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
import { KeyIcon } from "@phosphor-icons/react";
|
||||
|
||||
type SelectCredentialProps = {
|
||||
credentials: CredentialsMetaResponse[];
|
||||
value?: string;
|
||||
onChange: (credentialId: string) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const SelectCredential: React.FC<SelectCredentialProps> = ({
|
||||
credentials,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
label = "Credential",
|
||||
placeholder = "Select credential",
|
||||
}) => {
|
||||
const options = credentials.map((cred) => {
|
||||
const details: string[] = [];
|
||||
if (cred.title && cred.title !== cred.provider) {
|
||||
details.push(cred.title);
|
||||
}
|
||||
if (cred.username) {
|
||||
details.push(cred.username);
|
||||
}
|
||||
if (cred.host) {
|
||||
details.push(cred.host);
|
||||
}
|
||||
const label =
|
||||
details.length > 0
|
||||
? `${cred.provider} (${details.join(" - ")})`
|
||||
: cred.provider;
|
||||
|
||||
return {
|
||||
value: cred.id,
|
||||
label,
|
||||
icon: <KeyIcon className="h-4 w-4" />,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
id="select-credential"
|
||||
wrapperClassName="!mb-0"
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
options={options}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
size="small"
|
||||
hideLabel
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
|
||||
export const filterCredentialsByProvider = (
|
||||
credentials: CredentialsMetaResponse[] | undefined,
|
||||
provider: string[],
|
||||
) => {
|
||||
const filtered =
|
||||
credentials?.filter((credential) =>
|
||||
provider.includes(credential.provider),
|
||||
) ?? [];
|
||||
return {
|
||||
credentials: filtered,
|
||||
exists: filtered.length > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export function toDisplayName(provider: string): string {
|
||||
console.log("provider", provider);
|
||||
// Special cases that need manual handling
|
||||
const specialCases: Record<string, string> = {
|
||||
aiml_api: "AI/ML",
|
||||
d_id: "D-ID",
|
||||
e2b: "E2B",
|
||||
llama_api: "Llama API",
|
||||
open_router: "Open Router",
|
||||
smtp: "SMTP",
|
||||
revid: "Rev.ID",
|
||||
};
|
||||
|
||||
if (specialCases[provider]) {
|
||||
return specialCases[provider];
|
||||
}
|
||||
|
||||
// General case: convert snake_case to Title Case
|
||||
return provider
|
||||
.split(/[_-]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function isCredentialFieldSchema(schema: any): boolean {
|
||||
return (
|
||||
typeof schema === "object" &&
|
||||
schema !== null &&
|
||||
"credentials_provider" in schema
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useGetV1ListCredentials } from "@/app/api/__generated__/endpoints/integrations/integrations";
|
||||
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
|
||||
|
||||
export const useCredentialField = () => {
|
||||
// Fetch all the credentials from the backend
|
||||
// We will save it in cache for 10 min, if user edits the credential, we will invalidate the cache
|
||||
// Whenever user adds a block, we filter the credentials list and check if this block's provider is in the list
|
||||
const { data: credentials, isLoading: isCredentialListLoading } =
|
||||
useGetV1ListCredentials({
|
||||
query: {
|
||||
refetchInterval: 10 * 60 * 1000,
|
||||
select: (x) => {
|
||||
return x.data as CredentialsMetaResponse[];
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
credentials,
|
||||
isCredentialListLoading,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RegistryFieldsType } from "@rjsf/utils";
|
||||
import { CredentialsField } from "./CredentialField";
|
||||
import { CredentialsField } from "./CredentialField/CredentialField";
|
||||
import { AnyOfField } from "./AnyOfField/AnyOfField";
|
||||
import { ObjectField } from "./ObjectField";
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { generateHandleId } from "../../handlers/helpers";
|
||||
import { getTypeDisplayInfo } from "../helpers";
|
||||
import { ArrayEditorContext } from "../../components/ArrayEditor/ArrayEditorContext";
|
||||
import {
|
||||
isCredentialFieldSchema,
|
||||
toDisplayName,
|
||||
} from "../fields/CredentialField/helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
id,
|
||||
@@ -47,6 +52,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
}
|
||||
const isAnyOf = Array.isArray((schema as any)?.anyOf);
|
||||
const isOneOf = Array.isArray((schema as any)?.oneOf);
|
||||
const isCredential = isCredentialFieldSchema(schema);
|
||||
const suppressHandle = isAnyOf || isOneOf;
|
||||
|
||||
if (!showAdvanced && schema.advanced === true && !isConnected) {
|
||||
@@ -63,12 +69,17 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
|
||||
<div className="mt-4 w-[400px] space-y-1">
|
||||
{label && schema.type && (
|
||||
<label htmlFor={id} className="flex items-center gap-1">
|
||||
{!suppressHandle && !fromAnyOf && (
|
||||
{!suppressHandle && !fromAnyOf && !isCredential && (
|
||||
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
|
||||
)}
|
||||
{!fromAnyOf && (
|
||||
<Text variant="body" className="line-clamp-1">
|
||||
{label}
|
||||
<Text
|
||||
variant="body"
|
||||
className={cn("line-clamp-1", isCredential && "ml-3")}
|
||||
>
|
||||
{isCredential
|
||||
? toDisplayName(schema.credentials_provider[0]) + " credentials"
|
||||
: label}
|
||||
</Text>
|
||||
)}
|
||||
{!fromAnyOf && (
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"get": {
|
||||
"tags": ["v1", "integrations"],
|
||||
"summary": "List Credentials",
|
||||
"operationId": "getV1ListCredentials",
|
||||
"operationId": "getV1List credentials",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
@@ -123,7 +123,7 @@
|
||||
"$ref": "#/components/schemas/CredentialsMetaResponse"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Response Getv1Listcredentials"
|
||||
"title": "Response Getv1List Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,8 +268,8 @@
|
||||
"/api/integrations/{provider}/credentials/{cred_id}": {
|
||||
"get": {
|
||||
"tags": ["v1", "integrations"],
|
||||
"summary": "Get Credential",
|
||||
"operationId": "getV1GetCredential",
|
||||
"summary": "Get Specific Credential By ID",
|
||||
"operationId": "getV1Get specific credential by id",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -315,7 +315,7 @@
|
||||
"host_scoped": "#/components/schemas/HostScopedCredentials-Output"
|
||||
}
|
||||
},
|
||||
"title": "Response Getv1Getcredential"
|
||||
"title": "Response Getv1Get Specific Credential By Id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user