feat(frontend): Implement discriminator logic in the new builder’s credential system. (#11124)

- Depends on https://github.com/Significant-Gravitas/AutoGPT/pull/11107
and https://github.com/Significant-Gravitas/AutoGPT/pull/11122

In this PR, I’ve added support for discrimination. Now, users can choose
a credential type based on other input values.


https://github.com/user-attachments/assets/6cedc59b-ec84-4ae2-bb06-59d891916847

### Changes 🏗️
- Updated CredentialsField to utilize credentialProvider from schema.
- Refactored helper functions to filter credentials based on the
selected provider.
- Modified APIKeyCredentialsModal and PasswordCredentialsModal to accept
provider as a prop.
- Improved FieldTemplate to dynamically display the correct credential
provider.
- Added getCredentialProviderFromSchema function to manage
multi-provider scenarios.

### Checklist 📋

#### For code changes:
- [x] Credential input is correctly updating based on other input
values.
- [x] Credential can be added correctly.
This commit is contained in:
Abhimanyu Yadav
2025-10-13 17:38:10 +05:30
committed by GitHub
parent e32c509ccc
commit f67d78df3e
8 changed files with 127 additions and 83 deletions

View File

@@ -9,7 +9,13 @@ import { OAuthCredentialModal } from "./models/OAuthCredentialModal/OAuthCredent
import { PasswordCredentialsModal } from "./models/PasswordCredentialModal/PasswordCredentialModal";
export const CredentialsField = (props: FieldProps) => {
const { formData = {}, onChange, required: _required, schema } = props;
const {
formData = {},
onChange,
required: _required,
schema,
formContext,
} = props;
const {
credentials,
isCredentialListLoading,
@@ -17,8 +23,10 @@ export const CredentialsField = (props: FieldProps) => {
supportsOAuth2,
supportsUserPassword,
credentialsExists,
credentialProvider,
} = useCredentialField({
credentialSchema: schema as BlockIOCredentialsSubSchema,
nodeId: formContext.nodeId,
});
const setField = (key: string, value: any) =>
@@ -41,6 +49,10 @@ export const CredentialsField = (props: FieldProps) => {
);
}
if (!credentialProvider) {
return null;
}
return (
<div className="flex flex-col gap-2">
{credentialsExists && (
@@ -58,16 +70,14 @@ export const CredentialsField = (props: FieldProps) => {
{supportsApiKey && (
<APIKeyCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
provider={credentialProvider}
/>
)}
{supportsOAuth2 && (
<OAuthCredentialModal provider={schema.credentials_provider[0]} />
<OAuthCredentialModal provider={credentialProvider} />
)}
{supportsUserPassword && (
<PasswordCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
provider={schema.credentials_provider[0]}
/>
<PasswordCredentialsModal provider={credentialProvider} />
)}
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import {
GoogleLogoIcon,
KeyholeIcon,
@@ -12,12 +13,12 @@ import {
export const filterCredentialsByProvider = (
credentials: CredentialsMetaResponse[] | undefined,
provider: string[],
provider: string,
) => {
console.log("provider", provider);
console.log("credentials", credentials);
const filtered =
credentials?.filter((credential) =>
provider.includes(credential.provider),
) ?? [];
credentials?.filter((credential) => provider === credential.provider) ?? [];
return {
credentials: filtered,
exists: filtered.length > 0,
@@ -96,3 +97,41 @@ export const providerIcons: Partial<Record<string, Icon>> = {
todoist: KeyholeIcon,
zerobounce: KeyholeIcon,
};
export const getCredentialProviderFromSchema = (
formData: Record<string, any>,
schema: BlockIOCredentialsSubSchema,
) => {
const discriminator = schema.discriminator;
const discriminatorMapping = schema.discriminator_mapping;
const discriminatorValues = schema.discriminator_values;
const providers = schema.credentials_provider;
const discriminatorValue = [
discriminator ? formData[discriminator] : null,
...(discriminatorValues || []),
].find(Boolean);
const discriminatedProvider = discriminatorMapping
? discriminatorMapping[discriminatorValue]
: null;
if (providers.length > 1) {
if (!discriminator) {
throw new Error(
"Multi-provider credential input requires discriminator!",
);
}
if (!discriminatedProvider) {
console.warn(
`Missing discriminator value from '${discriminator}': ` +
"hiding credentials input until it is set.",
);
return null;
}
console.log("discriminatedProvider", discriminatedProvider);
return discriminatedProvider;
} else {
return providers[0];
}
};

View File

@@ -14,22 +14,12 @@ import { Text } from "@/components/atoms/Text/Text";
type Props = {
schema: BlockIOCredentialsSubSchema;
provider: string;
};
export function APIKeyCredentialsModal({ schema }: Props) {
const {
form,
isLoading,
schemaDescription,
onSubmit,
provider,
isOpen,
setIsOpen,
} = useAPIKeyCredentialsModal({ schema });
if (isLoading) {
return null;
}
export function APIKeyCredentialsModal({ schema, provider }: Props) {
const { form, schemaDescription, onSubmit, isOpen, setIsOpen } =
useAPIKeyCredentialsModal({ schema, provider });
return (
<>

View File

@@ -19,14 +19,14 @@ export type APIKeyFormValues = {
type useAPIKeyCredentialsModalType = {
schema: BlockIOCredentialsSubSchema;
provider: string;
};
export function useAPIKeyCredentialsModal({
schema,
provider,
}: useAPIKeyCredentialsModalType): {
form: UseFormReturn<APIKeyFormValues>;
isLoading: boolean;
provider: string;
schemaDescription?: string;
onSubmit: (values: APIKeyFormValues) => Promise<void>;
isOpen: boolean;
@@ -36,31 +36,30 @@ export function useAPIKeyCredentialsModal({
const [isOpen, setIsOpen] = useState(false);
const queryClient = useQueryClient();
const { mutateAsync: createCredentials, isPending: isCreatingCredentials } =
usePostV1CreateCredentials({
mutation: {
onSuccess: async () => {
form.reset();
setIsOpen(false);
toast({
title: "Success",
description: "Credentials created successfully",
variant: "default",
});
const { mutateAsync: createCredentials } = usePostV1CreateCredentials({
mutation: {
onSuccess: async () => {
form.reset();
setIsOpen(false);
toast({
title: "Success",
description: "Credentials created successfully",
variant: "default",
});
await queryClient.refetchQueries({
queryKey: getGetV1ListCredentialsQueryKey(),
});
},
onError: () => {
toast({
title: "Error",
description: "Failed to create credentials.",
variant: "destructive",
});
},
await queryClient.refetchQueries({
queryKey: getGetV1ListCredentialsQueryKey(),
});
},
});
onError: () => {
toast({
title: "Error",
description: "Failed to create credentials.",
variant: "destructive",
});
},
},
});
const formSchema = z.object({
apiKey: z.string().min(1, "API Key is required"),
@@ -83,9 +82,9 @@ export function useAPIKeyCredentialsModal({
: undefined;
createCredentials({
provider: schema.credentials_provider[0],
provider: provider,
data: {
provider: schema.credentials_provider[0],
provider: provider,
type: "api_key",
api_key: values.apiKey,
title: values.title,
@@ -96,8 +95,6 @@ export function useAPIKeyCredentialsModal({
return {
form,
isLoading: isCreatingCredentials,
provider: schema.credentials_provider[0],
schemaDescription: schema.description,
onSubmit,
isOpen,

View File

@@ -2,28 +2,18 @@ import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Form, FormField } from "@/components/__legacy__/ui/form";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
import { usePasswordCredentialModal } from "./usePasswordCredentialModal";
import { toDisplayName } from "../../helpers";
import { UserIcon } from "@phosphor-icons/react";
type Props = {
schema: BlockIOCredentialsSubSchema;
provider: string;
};
export function PasswordCredentialsModal({ schema, provider }: Props) {
const {
credentials,
isCredentialListLoading,
form,
onSubmit,
open,
setOpen,
} = usePasswordCredentialModal({ schema });
if (!credentials || isCredentialListLoading) {
return null;
}
export function PasswordCredentialsModal({ provider }: Props) {
const { form, onSubmit, open, setOpen } = usePasswordCredentialModal({
provider,
});
return (
<>

View File

@@ -1,7 +1,5 @@
import { useState } from "react";
import { useCredentialField } from "../../useCredentialField";
import z from "zod";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
@@ -12,18 +10,15 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
type usePasswordCredentialModalType = {
schema: BlockIOCredentialsSubSchema;
provider: string;
};
export const usePasswordCredentialModal = ({
schema,
provider,
}: usePasswordCredentialModalType) => {
const [open, setOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
const { credentials, isCredentialListLoading } = useCredentialField({
credentialSchema: schema,
});
const formSchema = z.object({
username: z.string().min(1, "Username is required"),
@@ -60,9 +55,9 @@ export const usePasswordCredentialModal = ({
async function onSubmit(values: z.infer<typeof formSchema>) {
createCredentials({
provider: schema.credentials_provider[0],
provider: provider,
data: {
provider: schema.credentials_provider[0],
provider: provider,
type: "user_password",
username: values.username,
password: values.password,
@@ -73,8 +68,6 @@ export const usePasswordCredentialModal = ({
return {
form,
credentials,
isCredentialListLoading,
onSubmit,
open,
setOpen,

View File

@@ -1,12 +1,18 @@
import { useGetV1ListCredentials } from "@/app/api/__generated__/endpoints/integrations/integrations";
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { filterCredentialsByProvider } from "./helpers";
import {
filterCredentialsByProvider,
getCredentialProviderFromSchema,
} from "./helpers";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
export const useCredentialField = ({
credentialSchema,
nodeId,
}: {
credentialSchema: BlockIOCredentialsSubSchema; // Here we are using manual typing, we need to fix it with automatic one
nodeId: string;
}) => {
// 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
@@ -21,14 +27,22 @@ export const useCredentialField = ({
},
});
const hardcodedValues = useNodeStore((state) =>
state.getHardCodedValues(nodeId),
);
const credentialProvider = getCredentialProviderFromSchema(
hardcodedValues,
credentialSchema,
);
const supportsApiKey = credentialSchema.credentials_types.includes("api_key");
const supportsOAuth2 = credentialSchema.credentials_types.includes("oauth2");
const supportsUserPassword =
credentialSchema.credentials_types.includes("user_password");
const credentialProviders = credentialSchema.credentials_provider;
const { credentials: filteredCredentials, exists: credentialsExists } =
filterCredentialsByProvider(credentials, credentialProviders);
filterCredentialsByProvider(credentials, credentialProvider ?? "");
return {
credentials: filteredCredentials,
@@ -37,5 +51,6 @@ export const useCredentialField = ({
supportsOAuth2,
supportsUserPassword,
credentialsExists,
credentialProvider,
};
};

View File

@@ -18,8 +18,10 @@ import { ArrayEditorContext } from "../../components/ArrayEditor/ArrayEditorCont
import {
isCredentialFieldSchema,
toDisplayName,
getCredentialProviderFromSchema,
} from "../fields/CredentialField/helpers";
import { cn } from "@/lib/utils";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { BlockUIType } from "@/lib/autogpt-server-api";
const FieldTemplate: React.FC<FieldTemplateProps> = ({
@@ -38,6 +40,7 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
const showAdvanced = useNodeStore(
(state) => state.nodeAdvancedStates[nodeId] ?? false,
);
const formData = useNodeStore((state) => state.getHardCodedValues(nodeId));
const { isArrayItem, arrayFieldHandleId } = useContext(ArrayEditorContext);
@@ -65,6 +68,13 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
const { displayType, colorClass } = getTypeDisplayInfo(schema);
let credentialProvider = null;
if (isCredential) {
credentialProvider = getCredentialProviderFromSchema(
formData,
schema as BlockIOCredentialsSubSchema,
);
}
if (formContext.uiType === BlockUIType.NOTE) {
return <div className="w-full space-y-1">{children}</div>;
}
@@ -85,8 +95,8 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
variant="body"
className={cn("line-clamp-1", isCredential && "ml-3")}
>
{isCredential
? toDisplayName(schema.credentials_provider[0]) + " credentials"
{isCredential && credentialProvider
? toDisplayName(credentialProvider) + " credentials"
: label}
</Text>
)}