Skip to content

Commit c42f94c

Browse files
authored
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
1 parent 4e1557e commit c42f94c

File tree

11 files changed

+201
-45
lines changed

11 files changed

+201
-45
lines changed

autogpt_platform/backend/backend/server/integrations/router.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async def callback(
180180
)
181181

182182

183-
@router.get("/credentials")
183+
@router.get("/credentials", summary="List Credentials")
184184
async def list_credentials(
185185
user_id: Annotated[str, Security(get_user_id)],
186186
) -> list[CredentialsMetaResponse]:
@@ -221,7 +221,9 @@ async def list_credentials_by_provider(
221221
]
222222

223223

224-
@router.get("/{provider}/credentials/{cred_id}")
224+
@router.get(
225+
"/{provider}/credentials/{cred_id}", summary="Get Specific Credential By ID"
226+
)
225227
async def get_credential(
226228
provider: Annotated[
227229
ProviderName, Path(title="The provider to retrieve credentials for")

autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/fields/CredentialField.tsx

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import { FieldProps } from "@rjsf/utils";
3+
import { useCredentialField } from "./useCredentialField";
4+
import { filterCredentialsByProvider } from "./helpers";
5+
import { PlusIcon } from "@phosphor-icons/react";
6+
import { Button } from "@/components/atoms/Button/Button";
7+
import { SelectCredential } from "./SelectCredential";
8+
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
9+
10+
export const CredentialsField = (props: FieldProps) => {
11+
const { formData = {}, onChange, required: _required, schema } = props;
12+
const { credentials, isCredentialListLoading } = useCredentialField();
13+
14+
const credentialProviders = schema.credentials_provider;
15+
const { credentials: filteredCredentials, exists: credentialsExists } =
16+
filterCredentialsByProvider(credentials, credentialProviders);
17+
18+
const setField = (key: string, value: any) =>
19+
onChange({ ...formData, [key]: value });
20+
21+
if (isCredentialListLoading) {
22+
return (
23+
<div className="flex flex-col gap-2">
24+
<Skeleton className="h-8 w-full rounded-xlarge" />
25+
<Skeleton className="h-8 w-[30%] rounded-xlarge" />
26+
</div>
27+
);
28+
}
29+
30+
return (
31+
<div className="flex flex-col gap-2">
32+
{credentialsExists && (
33+
<SelectCredential
34+
credentials={filteredCredentials}
35+
value={formData.id}
36+
onChange={(value) => setField("id", value)}
37+
disabled={false}
38+
label="Credential"
39+
placeholder="Select credential"
40+
/>
41+
)}
42+
43+
{/* TODO : We need to add a modal to add a new credential */}
44+
<Button type="button" className="w-fit" size="small">
45+
<PlusIcon /> Add API Key
46+
</Button>
47+
</div>
48+
);
49+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import { Select } from "@/components/atoms/Select/Select";
3+
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
4+
import { KeyIcon } from "@phosphor-icons/react";
5+
6+
type SelectCredentialProps = {
7+
credentials: CredentialsMetaResponse[];
8+
value?: string;
9+
onChange: (credentialId: string) => void;
10+
disabled?: boolean;
11+
label?: string;
12+
placeholder?: string;
13+
};
14+
15+
export const SelectCredential: React.FC<SelectCredentialProps> = ({
16+
credentials,
17+
value,
18+
onChange,
19+
disabled = false,
20+
label = "Credential",
21+
placeholder = "Select credential",
22+
}) => {
23+
const options = credentials.map((cred) => {
24+
const details: string[] = [];
25+
if (cred.title && cred.title !== cred.provider) {
26+
details.push(cred.title);
27+
}
28+
if (cred.username) {
29+
details.push(cred.username);
30+
}
31+
if (cred.host) {
32+
details.push(cred.host);
33+
}
34+
const label =
35+
details.length > 0
36+
? `${cred.provider} (${details.join(" - ")})`
37+
: cred.provider;
38+
39+
return {
40+
value: cred.id,
41+
label,
42+
icon: <KeyIcon className="h-4 w-4" />,
43+
};
44+
});
45+
46+
return (
47+
<Select
48+
label={label}
49+
id="select-credential"
50+
wrapperClassName="!mb-0"
51+
value={value}
52+
onValueChange={onChange}
53+
options={options}
54+
disabled={disabled}
55+
placeholder={placeholder}
56+
size="small"
57+
hideLabel
58+
/>
59+
);
60+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
2+
3+
export const filterCredentialsByProvider = (
4+
credentials: CredentialsMetaResponse[] | undefined,
5+
provider: string[],
6+
) => {
7+
const filtered =
8+
credentials?.filter((credential) =>
9+
provider.includes(credential.provider),
10+
) ?? [];
11+
return {
12+
credentials: filtered,
13+
exists: filtered.length > 0,
14+
};
15+
};
16+
17+
export function toDisplayName(provider: string): string {
18+
console.log("provider", provider);
19+
// Special cases that need manual handling
20+
const specialCases: Record<string, string> = {
21+
aiml_api: "AI/ML",
22+
d_id: "D-ID",
23+
e2b: "E2B",
24+
llama_api: "Llama API",
25+
open_router: "Open Router",
26+
smtp: "SMTP",
27+
revid: "Rev.ID",
28+
};
29+
30+
if (specialCases[provider]) {
31+
return specialCases[provider];
32+
}
33+
34+
// General case: convert snake_case to Title Case
35+
return provider
36+
.split(/[_-]/)
37+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
38+
.join(" ");
39+
}
40+
41+
export function isCredentialFieldSchema(schema: any): boolean {
42+
return (
43+
typeof schema === "object" &&
44+
schema !== null &&
45+
"credentials_provider" in schema
46+
);
47+
}

autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/fields/CredentialField/types.ts

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useGetV1ListCredentials } from "@/app/api/__generated__/endpoints/integrations/integrations";
2+
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
3+
4+
export const useCredentialField = () => {
5+
// Fetch all the credentials from the backend
6+
// We will save it in cache for 10 min, if user edits the credential, we will invalidate the cache
7+
// Whenever user adds a block, we filter the credentials list and check if this block's provider is in the list
8+
const { data: credentials, isLoading: isCredentialListLoading } =
9+
useGetV1ListCredentials({
10+
query: {
11+
refetchInterval: 10 * 60 * 1000,
12+
select: (x) => {
13+
return x.data as CredentialsMetaResponse[];
14+
},
15+
},
16+
});
17+
return {
18+
credentials,
19+
isCredentialListLoading,
20+
};
21+
};

autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/fields/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RegistryFieldsType } from "@rjsf/utils";
2-
import { CredentialsField } from "./CredentialField";
2+
import { CredentialsField } from "./CredentialField/CredentialField";
33
import { AnyOfField } from "./AnyOfField/AnyOfField";
44
import { ObjectField } from "./ObjectField";
55

autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/templates/FieldTemplate.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
1515
import { generateHandleId } from "../../handlers/helpers";
1616
import { getTypeDisplayInfo } from "../helpers";
1717
import { ArrayEditorContext } from "../../components/ArrayEditor/ArrayEditorContext";
18+
import {
19+
isCredentialFieldSchema,
20+
toDisplayName,
21+
} from "../fields/CredentialField/helpers";
22+
import { cn } from "@/lib/utils";
1823

1924
const FieldTemplate: React.FC<FieldTemplateProps> = ({
2025
id,
@@ -47,6 +52,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
4752
}
4853
const isAnyOf = Array.isArray((schema as any)?.anyOf);
4954
const isOneOf = Array.isArray((schema as any)?.oneOf);
55+
const isCredential = isCredentialFieldSchema(schema);
5056
const suppressHandle = isAnyOf || isOneOf;
5157

5258
if (!showAdvanced && schema.advanced === true && !isConnected) {
@@ -63,12 +69,17 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
6369
<div className="mt-4 w-[400px] space-y-1">
6470
{label && schema.type && (
6571
<label htmlFor={id} className="flex items-center gap-1">
66-
{!suppressHandle && !fromAnyOf && (
72+
{!suppressHandle && !fromAnyOf && !isCredential && (
6773
<NodeHandle id={fieldKey} isConnected={isConnected} side="left" />
6874
)}
6975
{!fromAnyOf && (
70-
<Text variant="body" className="line-clamp-1">
71-
{label}
76+
<Text
77+
variant="body"
78+
className={cn("line-clamp-1", isCredential && "ml-3")}
79+
>
80+
{isCredential
81+
? toDisplayName(schema.credentials_provider[0]) + " credentials"
82+
: label}
7283
</Text>
7384
)}
7485
{!fromAnyOf && (

autogpt_platform/frontend/src/app/(platform)/build/stores/credentialStore.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)