Merge branch 'dev' into swiftyos/caching-pt2

This commit is contained in:
Swifty
2025-10-07 14:06:44 +02:00
committed by GitHub
13 changed files with 414 additions and 64 deletions

View File

@@ -244,7 +244,7 @@ async def get_credential(
return credential
@router.post("/{provider}/credentials", status_code=201)
@router.post("/{provider}/credentials", status_code=201, summary="Create Credentials")
async def create_credentials(
user_id: Annotated[str, Security(get_user_id)],
provider: Annotated[

View File

@@ -7,10 +7,8 @@ import { useMemo } from "react";
import { CustomNode } from "./nodes/CustomNode";
import { useCustomEdge } from "./edges/useCustomEdge";
import CustomEdge from "./edges/CustomEdge";
import { RightSidebar } from "../RIghtSidebar";
export const Flow = () => {
// All these 3 are working perfectly
const nodes = useNodeStore(useShallow((state) => state.nodes));
const onNodesChange = useNodeStore(
useShallow((state) => state.onNodesChange),
@@ -20,7 +18,6 @@ export const Flow = () => {
return (
<div className="flex h-full w-full dark:bg-slate-900">
{/* Builder area - flexible width */}
<div className="relative flex-1">
<ReactFlow
nodes={nodes}
@@ -36,9 +33,6 @@ export const Flow = () => {
<NewControlPanel />
</ReactFlow>
</div>
<div className="w-[30%]">
<RightSidebar />
</div>
</div>
);
};

View File

@@ -42,7 +42,7 @@ const CustomEdge = ({
<EdgeLabelRenderer>
<Button
onClick={() => removeConnection(id)}
className={`absolute z-10 min-w-0 p-1`}
className={`absolute z-10 h-fit min-w-0 p-1`}
variant="secondary"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,

View File

@@ -32,8 +32,8 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
return (
<div
className={cn(
"rounded-xl border border-slate-200/60 bg-gradient-to-br from-white to-slate-50/30 shadow-lg shadow-slate-900/5 backdrop-blur-sm",
selected && "border-2 border-slate-200 shadow-2xl",
"rounded-xl bg-gradient-to-br from-white to-slate-50/30 shadow-lg shadow-slate-900/5 ring-1 ring-slate-200/60 backdrop-blur-sm",
selected && "shadow-2xl ring-2 ring-slate-200",
)}
>
{/* Header */}

View File

@@ -29,7 +29,7 @@ export const OutputHandler = ({
<div className="flex flex-col items-end justify-between gap-2 rounded-b-xl border-t border-slate-200/50 bg-white py-3.5">
<Button
variant="ghost"
className="mr-4 p-0"
className="mr-4 h-fit min-w-0 p-0 hover:border-transparent hover:bg-transparent"
onClick={() => setIsOutputVisible(!isOutputVisible)}
>
<Text
@@ -54,30 +54,27 @@ export const OutputHandler = ({
return shouldShow ? (
<div key={key} className="relative flex items-center gap-2">
<Text
variant="body"
className="flex items-center gap-2 font-medium text-slate-700"
>
{property?.description && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
style={{ marginLeft: 6, cursor: "pointer" }}
aria-label="info"
tabIndex={0}
>
<InfoIcon />
</span>
</TooltipTrigger>
<TooltipContent>{property?.description}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{property?.description && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
style={{ marginLeft: 6, cursor: "pointer" }}
aria-label="info"
tabIndex={0}
>
<InfoIcon />
</span>
</TooltipTrigger>
<TooltipContent>{property?.description}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Text variant="body" className="text-slate-700">
{property?.title || key}{" "}
<Text variant="small" as="span" className={colorClass}>
({displayType})
</Text>
</Text>
<Text variant="small" as="span" className={colorClass}>
({displayType})
</Text>
<NodeHandle id={key} isConnected={isConnected} side="right" />
</div>

View File

@@ -1,23 +1,42 @@
import React from "react";
import React, { useEffect } from "react";
import { FieldProps } from "@rjsf/utils";
import { useCredentialField } from "./useCredentialField";
import { filterCredentialsByProvider } from "./helpers";
import { PlusIcon } from "@phosphor-icons/react";
import { KeyIcon, PlusIcon } from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import { SelectCredential } from "./SelectCredential";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { APIKeyCredentialsModal } from "./models/APIKeyCredentialModal/APIKeyCredentialModal";
import { Text } from "@/components/atoms/Text/Text";
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 {
credentials,
isCredentialListLoading,
supportsApiKey,
supportsOAuth2,
isAPIKeyModalOpen,
setIsAPIKeyModalOpen,
credentialsExists,
} = useCredentialField({
credentialSchema: schema as BlockIOCredentialsSubSchema,
});
const setField = (key: string, value: any) =>
onChange({ ...formData, [key]: value });
useEffect(() => {
if (!isCredentialListLoading && credentials.length > 0 && !formData.id) {
const latestCredential = credentials[credentials.length - 1];
setField("id", latestCredential.id);
}
}, [isCredentialListLoading, credentials, formData.id]);
const handleCredentialCreated = (credentialId: string) => {
setField("id", credentialId);
};
if (isCredentialListLoading) {
return (
<div className="flex flex-col gap-2">
@@ -31,7 +50,7 @@ export const CredentialsField = (props: FieldProps) => {
<div className="flex flex-col gap-2">
{credentialsExists && (
<SelectCredential
credentials={filteredCredentials}
credentials={credentials}
value={formData.id}
onChange={(value) => setField("id", value)}
disabled={false}
@@ -40,10 +59,35 @@ export const CredentialsField = (props: FieldProps) => {
/>
)}
{/* 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>
{supportsApiKey && (
<>
<APIKeyCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
open={isAPIKeyModalOpen}
onClose={() => setIsAPIKeyModalOpen(false)}
onSuccess={handleCredentialCreated}
/>
<Button
type="button"
className="w-auto min-w-0"
size="small"
onClick={() => setIsAPIKeyModalOpen(true)}
>
<KeyIcon />
<Text variant="body-medium" className="!text-white opacity-100">
Add API key
</Text>
</Button>
</>
)}
{supportsOAuth2 && (
<Button type="button" className="w-fit" size="small">
<PlusIcon />
Add OAuth2
</Button>
)}
</div>
</div>
);
};

View File

@@ -1,7 +1,9 @@
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";
import { ArrowSquareOutIcon, KeyIcon } from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import Link from "next/link";
type SelectCredentialProps = {
credentials: CredentialsMetaResponse[];
@@ -44,17 +46,24 @@ export const SelectCredential: React.FC<SelectCredentialProps> = ({
});
return (
<Select
label={label}
id="select-credential"
wrapperClassName="!mb-0"
value={value}
onValueChange={onChange}
options={options}
disabled={disabled}
placeholder={placeholder}
size="small"
hideLabel
/>
<div className="flex w-full items-center gap-2">
<Select
label={label}
id="select-credential"
wrapperClassName="!mb-0 flex-1"
value={value}
onValueChange={onChange}
options={options}
disabled={disabled}
placeholder={placeholder}
size="small"
hideLabel
/>
<Link href={`/profile/integrations`}>
<Button variant="outline" size="icon" className="h-8 w-8 p-0">
<ArrowSquareOutIcon className="h-4 w-4" />
</Button>
</Link>
</div>
);
};

View File

@@ -1,4 +1,14 @@
import { CredentialsMetaResponse } from "@/app/api/__generated__/models/credentialsMetaResponse";
// Need to replace these icons with phosphor icons
import {
FaDiscord,
FaMedium,
FaGithub,
FaGoogle,
FaHubspot,
FaTwitter,
} from "react-icons/fa";
import { GoogleLogoIcon, KeyIcon, NotionLogoIcon } from "@phosphor-icons/react";
export const filterCredentialsByProvider = (
credentials: CredentialsMetaResponse[] | undefined,
@@ -45,3 +55,47 @@ export function isCredentialFieldSchema(schema: any): boolean {
"credentials_provider" in schema
);
}
export const providerIcons: Partial<
Record<string, React.FC<{ className?: string }>>
> = {
aiml_api: KeyIcon,
anthropic: KeyIcon,
apollo: KeyIcon,
e2b: KeyIcon,
github: FaGithub,
google: GoogleLogoIcon,
groq: KeyIcon,
http: KeyIcon,
notion: NotionLogoIcon,
nvidia: KeyIcon,
discord: FaDiscord,
d_id: KeyIcon,
google_maps: FaGoogle,
jina: KeyIcon,
ideogram: KeyIcon,
linear: KeyIcon,
medium: FaMedium,
mem0: KeyIcon,
ollama: KeyIcon,
openai: KeyIcon,
openweathermap: KeyIcon,
open_router: KeyIcon,
llama_api: KeyIcon,
pinecone: KeyIcon,
enrichlayer: KeyIcon,
slant3d: KeyIcon,
screenshotone: KeyIcon,
smtp: KeyIcon,
replicate: KeyIcon,
reddit: KeyIcon,
fal: KeyIcon,
revid: KeyIcon,
twitter: FaTwitter,
unreal_speech: KeyIcon,
exa: KeyIcon,
hubspot: FaHubspot,
smartlead: KeyIcon,
todoist: KeyIcon,
zerobounce: KeyIcon,
};

View File

@@ -0,0 +1,119 @@
import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
Form,
FormDescription,
FormField,
} from "@/components/__legacy__/ui/form";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types"; // we need to find a way to replace it with autogenerated types
import { useAPIKeyCredentialsModal } from "./useAPIKeyCredentialsModal";
import { toDisplayName } from "../../helpers";
type Props = {
schema: BlockIOCredentialsSubSchema;
open: boolean;
onClose: () => void;
onSuccess: (credentialId: string) => void;
};
export function APIKeyCredentialsModal({
schema,
open,
onClose,
onSuccess,
}: Props) {
const { form, isLoading, schemaDescription, onSubmit, provider } =
useAPIKeyCredentialsModal({ schema, onClose, onSuccess });
if (isLoading) {
return null;
}
return (
<Dialog
title={`Add new API key for ${toDisplayName(provider) ?? ""}`}
controlled={{
isOpen: open,
set: (isOpen) => {
if (!isOpen) onClose();
},
}}
onClose={onClose}
styling={{
maxWidth: "25rem",
}}
>
<Dialog.Content>
{schemaDescription && (
<p className="mb-4 text-sm text-zinc-600">{schemaDescription}</p>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<>
<Input
id="apiKey"
label="API Key"
type="password"
placeholder="Enter API key..."
size="small"
hint={
schema.credentials_scopes ? (
<FormDescription>
Required scope(s) for this block:{" "}
{schema.credentials_scopes?.map((s, i, a) => (
<span key={i}>
<code className="text-xs font-bold">{s}</code>
{i < a.length - 1 && ", "}
</span>
))}
</FormDescription>
) : null
}
{...field}
/>
</>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this API key..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="expiresAt"
render={({ field }) => (
<Input
id="expiresAt"
label="Expiration Date"
type="datetime-local"
placeholder="Select expiration date..."
size="small"
{...field}
/>
)}
/>
<Button type="submit" size="small" className="min-w-68">
Save & use this API key
</Button>
</form>
</Form>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -0,0 +1,111 @@
import { z } from "zod";
import { useForm, type UseFormReturn } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
import {
getGetV1ListCredentialsQueryKey,
usePostV1CreateCredentials,
} from "@/app/api/__generated__/endpoints/integrations/integrations";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { APIKeyCredentials } from "@/app/api/__generated__/models/aPIKeyCredentials";
import { useQueryClient } from "@tanstack/react-query";
import { PostV1CreateCredentials201 } from "@/app/api/__generated__/models/postV1CreateCredentials201";
export type APIKeyFormValues = {
apiKey: string;
title: string;
expiresAt?: string;
};
type useAPIKeyCredentialsModalType = {
schema: BlockIOCredentialsSubSchema;
onClose: () => void;
onSuccess: (credentialId: string) => void;
};
export function useAPIKeyCredentialsModal({
schema,
onClose,
onSuccess,
}: useAPIKeyCredentialsModalType): {
form: UseFormReturn<APIKeyFormValues>;
isLoading: boolean;
provider: string;
schemaDescription?: string;
onSubmit: (values: APIKeyFormValues) => Promise<void>;
} {
const { toast } = useToast();
const queryClient = useQueryClient();
const { mutateAsync: createCredentials, isPending: isCreatingCredentials } =
usePostV1CreateCredentials({
mutation: {
onSuccess: async (response) => {
const credentialId = (response.data as PostV1CreateCredentials201)
?.id;
onClose();
form.reset();
toast({
title: "Success",
description: "Credentials created successfully",
variant: "default",
});
await queryClient.refetchQueries({
queryKey: getGetV1ListCredentialsQueryKey(),
});
if (credentialId && onSuccess) {
onSuccess(credentialId);
}
},
onError: () => {
toast({
title: "Error",
description: "Failed to create credentials.",
variant: "destructive",
});
},
},
});
const formSchema = z.object({
apiKey: z.string().min(1, "API Key is required"),
title: z.string().min(1, "Name is required"),
expiresAt: z.string().optional(),
});
const form = useForm<APIKeyFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
apiKey: "",
title: "",
expiresAt: "",
},
});
async function onSubmit(values: APIKeyFormValues) {
const expiresAt = values.expiresAt
? new Date(values.expiresAt).getTime() / 1000
: undefined;
createCredentials({
provider: schema.credentials_provider[0],
data: {
provider: schema.credentials_provider[0],
type: "api_key",
api_key: values.apiKey,
title: values.title,
expires_at: expiresAt,
} as APIKeyCredentials,
});
}
return {
form,
isLoading: isCreatingCredentials,
provider: schema.credentials_provider[0],
schemaDescription: schema.description,
onSubmit,
};
}

View File

@@ -1,7 +1,16 @@
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 { useState } from "react";
import { filterCredentialsByProvider } from "./helpers";
export const useCredentialField = ({
credentialSchema,
}: {
credentialSchema: BlockIOCredentialsSubSchema; // Here we are using manual typing, we need to fix it with automatic one
}) => {
const [isAPIKeyModalOpen, setIsAPIKeyModalOpen] = useState(false);
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
@@ -14,8 +23,21 @@ export const useCredentialField = () => {
},
},
});
const supportsApiKey = credentialSchema.credentials_types.includes("api_key");
const supportsOAuth2 = credentialSchema.credentials_types.includes("oauth2");
const credentialProviders = credentialSchema.credentials_provider;
const { credentials: filteredCredentials, exists: credentialsExists } =
filterCredentialsByProvider(credentials, credentialProviders);
return {
credentials,
credentials: filteredCredentials,
isCredentialListLoading,
supportsApiKey,
supportsOAuth2,
isAPIKeyModalOpen,
setIsAPIKeyModalOpen,
credentialsExists,
};
};

View File

@@ -184,7 +184,7 @@
"post": {
"tags": ["v1", "integrations"],
"summary": "Create Credentials",
"operationId": "postV1CreateCredentials",
"operationId": "postV1Create credentials",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
@@ -246,7 +246,7 @@
"host_scoped": "#/components/schemas/HostScopedCredentials-Output"
}
},
"title": "Response Postv1Createcredentials"
"title": "Response Postv1Create Credentials"
}
}
}