mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user