From b811d487d2fc43c4a66800979dfff28359d4e6eb Mon Sep 17 00:00:00 2001 From: Ryan Hopper-Lowe <46546486+ryanhopperlowe@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:51:07 -0600 Subject: [PATCH] Chore/admin/update-environment-variables (#954) * enhance: move workflow env variable form to dialog Signed-off-by: Ryan Hopper-Lowe * feat: implement UI apis for agent/workflow environment variable updates * feat: add environment variables section to agents * chore: remove authenticate button from workflows page * feat: enhance AgentEnvSection to support updates and improve UI for environment variables - Added `onUpdate` prop to `AgentEnvSection` for handling updates to environment variables for both agents and workflows. - Updated the `AgentEnvSection` component to include a new UI for displaying and managing environment variables. - Refactored the `SelectList` component to allow optional removal of items and added customizable class names. - Introduced `EnvVariable` type to better structure environment variable data. - Modified API service to filter out empty environment variable entries during updates. This update improves the user experience when managing environment variables in both agents and workflows. * refactor: clean up SelectList component - Replaced static asterisks with a dynamic bullet point representation in the AgentEnvSection for better visual clarity. - Removed unnecessary padding from the SelectList component to streamline the layout. --------- Signed-off-by: Ryan Hopper-Lowe --- ui/admin/app/components/agent/Agent.tsx | 16 ++- .../shared/AgentEnvironmentVariableForm.tsx | 54 ++++++++ .../shared/EnvironmentVariableSection.tsx | 118 ++++++++++++++++++ .../composed/NameDescriptionForm.tsx | 6 +- .../app/components/composed/SelectModule.tsx | 42 +++++-- ui/admin/app/components/workflow/Workflow.tsx | 38 ++---- .../components/workflow/WorkflowEnvForm.tsx | 26 ---- ui/admin/app/lib/model/agents.ts | 2 + .../app/lib/model/environmentVariables.ts | 6 + ui/admin/app/lib/model/workflows.ts | 7 -- ui/admin/app/lib/routers/apiRoutes.ts | 4 + .../lib/service/api/EnvironmentApiService.tsx | 30 +++++ 12 files changed, 271 insertions(+), 78 deletions(-) create mode 100644 ui/admin/app/components/agent/shared/AgentEnvironmentVariableForm.tsx create mode 100644 ui/admin/app/components/agent/shared/EnvironmentVariableSection.tsx delete mode 100644 ui/admin/app/components/workflow/WorkflowEnvForm.tsx create mode 100644 ui/admin/app/lib/model/environmentVariables.ts create mode 100644 ui/admin/app/lib/service/api/EnvironmentApiService.tsx diff --git a/ui/admin/app/components/agent/Agent.tsx b/ui/admin/app/components/agent/Agent.tsx index 6a27101c..14a614f9 100644 --- a/ui/admin/app/components/agent/Agent.tsx +++ b/ui/admin/app/components/agent/Agent.tsx @@ -1,4 +1,4 @@ -import { LibraryIcon, PlusIcon, WrenchIcon } from "lucide-react"; +import { LibraryIcon, PlusIcon, VariableIcon, WrenchIcon } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import useSWR from "swr"; @@ -12,6 +12,7 @@ import { AgentForm } from "~/components/agent/AgentForm"; import { AgentPublishStatus } from "~/components/agent/AgentPublishStatus"; import { PastThreads } from "~/components/agent/PastThreads"; import { ToolForm } from "~/components/agent/ToolForm"; +import { EnvironmentVariableSection } from "~/components/agent/shared/EnvironmentVariableSection"; import { AgentKnowledgePanel } from "~/components/knowledge"; import { Button } from "~/components/ui/button"; import { CardDescription } from "~/components/ui/card"; @@ -125,6 +126,19 @@ export function Agent({ className, onRefresh }: AgentProps) { /> +
+ + + Environment Variables + + + +
+
diff --git a/ui/admin/app/components/agent/shared/AgentEnvironmentVariableForm.tsx b/ui/admin/app/components/agent/shared/AgentEnvironmentVariableForm.tsx new file mode 100644 index 00000000..f86676e9 --- /dev/null +++ b/ui/admin/app/components/agent/shared/AgentEnvironmentVariableForm.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; + +import { RevealedEnv } from "~/lib/model/environmentVariables"; + +import { NameDescriptionForm } from "~/components/composed/NameDescriptionForm"; +import { Button } from "~/components/ui/button"; + +type EnvFormProps = { + defaultValues: RevealedEnv; + onSubmit: (values: RevealedEnv) => void; + isLoading: boolean; +}; + +export function EnvForm({ + defaultValues, + onSubmit: updateEnv, + isLoading, +}: EnvFormProps) { + const [state, setState] = useState(() => + Object.entries(defaultValues).map(([name, description]) => ({ + name, + description, + })) + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (defaultValues) { + const updates = Object.fromEntries( + state.map(({ name, description }) => [name, description]) + ); + + updateEnv(updates); + } + }; + + return ( +
+ + + + + ); +} diff --git a/ui/admin/app/components/agent/shared/EnvironmentVariableSection.tsx b/ui/admin/app/components/agent/shared/EnvironmentVariableSection.tsx new file mode 100644 index 00000000..6f6ea688 --- /dev/null +++ b/ui/admin/app/components/agent/shared/EnvironmentVariableSection.tsx @@ -0,0 +1,118 @@ +import { PenIcon } from "lucide-react"; +import { toast } from "sonner"; + +import { Agent } from "~/lib/model/agents"; +import { EnvVariable } from "~/lib/model/environmentVariables"; +import { Workflow } from "~/lib/model/workflows"; +import { EnvironmentApiService } from "~/lib/service/api/EnvironmentApiService"; + +import { TypographyP } from "~/components/Typography"; +import { EnvForm } from "~/components/agent/shared/AgentEnvironmentVariableForm"; +import { SelectList } from "~/components/composed/SelectModule"; +import { Button } from "~/components/ui/button"; +import { Card } from "~/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { useAsync } from "~/hooks/useAsync"; + +type EnvironmentVariableSectionProps = { + entity: Agent | Workflow; + entityType: "agent" | "workflow"; + onUpdate: (env: Partial) => void; +}; + +export function EnvironmentVariableSection({ + entity, + entityType, + onUpdate, +}: EnvironmentVariableSectionProps) { + const revealEnv = useAsync(EnvironmentApiService.getEnvVariables); + + const onOpenChange = (open: boolean) => { + if (open) { + revealEnv.execute(entity.id); + } else { + revealEnv.clear(); + } + }; + + const updateEnv = useAsync(EnvironmentApiService.updateEnvVariables, { + onSuccess: (_, params) => { + toast.success("Environment variables updated"); + revealEnv.clear(); + + onUpdate({ + env: Object.keys(params[1]).map((name) => ({ + name, + value: "", + })), + }); + }, + }); + + const open = !!revealEnv.data; + + const items = entity.env ?? []; + + return ( +
+ + item.name} + items={items} + renderItem={renderItem} + selected={items.map((item) => item.name)} + /> + + + + + + + + + + Environment Variables + + + + Environment variables are used to store values that can + be used in your {entityType}. + + + {revealEnv.data && ( + + updateEnv.execute(entity.id, values) + } + /> + )} + + +
+ ); + + function renderItem(item: EnvVariable) { + return ( +
+ {item.name} + {"•".repeat(15)} +
+ ); + } +} diff --git a/ui/admin/app/components/composed/NameDescriptionForm.tsx b/ui/admin/app/components/composed/NameDescriptionForm.tsx index ad744e26..0e66e619 100644 --- a/ui/admin/app/components/composed/NameDescriptionForm.tsx +++ b/ui/admin/app/components/composed/NameDescriptionForm.tsx @@ -68,24 +68,25 @@ export function NameDescriptionForm({ key={field.id} > + {onRemove && ( + + )}
))} diff --git a/ui/admin/app/components/workflow/Workflow.tsx b/ui/admin/app/components/workflow/Workflow.tsx index 0ff87f12..058dfccf 100644 --- a/ui/admin/app/components/workflow/Workflow.tsx +++ b/ui/admin/app/components/workflow/Workflow.tsx @@ -1,22 +1,14 @@ -import { - Library, - List, - LockIcon, - PuzzleIcon, - Variable, - WrenchIcon, -} from "lucide-react"; +import { Library, List, PuzzleIcon, Variable, WrenchIcon } from "lucide-react"; import { useCallback, useState } from "react"; import { Workflow as WorkflowType } from "~/lib/model/workflows"; -import { WorkflowService } from "~/lib/service/api/workflowService"; import { cn } from "~/lib/utils"; import { TypographyH4, TypographyP } from "~/components/Typography"; import { AgentForm } from "~/components/agent"; +import { EnvironmentVariableSection } from "~/components/agent/shared/EnvironmentVariableSection"; import { AgentKnowledgePanel } from "~/components/knowledge"; import { BasicToolForm } from "~/components/tools/BasicToolForm"; -import { Button } from "~/components/ui/button"; import { CardDescription } from "~/components/ui/card"; import { ScrollArea } from "~/components/ui/scroll-area"; import { ParamsForm } from "~/components/workflow/ParamsForm"; @@ -24,9 +16,7 @@ import { WorkflowProvider, useWorkflow, } from "~/components/workflow/WorkflowContext"; -import { WorkflowEnvForm } from "~/components/workflow/WorkflowEnvForm"; import { StepsForm } from "~/components/workflow/steps/StepsForm"; -import { useAsync } from "~/hooks/useAsync"; import { useDebounce } from "~/hooks/useDebounce"; type WorkflowProps = { @@ -43,7 +33,7 @@ export function Workflow(props: WorkflowProps) { ); } -function WorkflowContent({ className, onPersistThreadId }: WorkflowProps) { +function WorkflowContent({ className }: WorkflowProps) { const { workflow, updateWorkflow, isUpdating, lastUpdated } = useWorkflow(); const [workflowUpdates, setWorkflowUpdates] = useState(workflow); @@ -65,10 +55,6 @@ function WorkflowContent({ className, onPersistThreadId }: WorkflowProps) { const debouncedSetWorkflowInfo = useDebounce(partialSetWorkflow, 1000); - const authenticate = useAsync(WorkflowService.authenticateWorkflow, { - onSuccess: ({ threadId }) => onPersistThreadId(threadId), - }); - return (
@@ -103,9 +89,10 @@ function WorkflowContent({ className, onPersistThreadId }: WorkflowProps) { Environment Variables -
@@ -174,17 +161,6 @@ function WorkflowContent({ className, onPersistThreadId }: WorkflowProps) { ) : (
)} - -
- -
); diff --git a/ui/admin/app/components/workflow/WorkflowEnvForm.tsx b/ui/admin/app/components/workflow/WorkflowEnvForm.tsx deleted file mode 100644 index 1a3425c5..00000000 --- a/ui/admin/app/components/workflow/WorkflowEnvForm.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Workflow } from "~/lib/model/workflows"; - -import { NameDescriptionForm } from "~/components/composed/NameDescriptionForm"; - -type WorkflowEnvFormProps = { - workflow: Workflow; - onChange?: (values: { env: Workflow["env"] }) => void; -}; - -export function WorkflowEnvForm({ workflow, onChange }: WorkflowEnvFormProps) { - return ( - - onChange?.({ - env: values.map((item) => ({ ...item, value: "" })), - }) - } - /> - ); -} diff --git a/ui/admin/app/lib/model/agents.ts b/ui/admin/app/lib/model/agents.ts index 23f19c7f..409ae0f1 100644 --- a/ui/admin/app/lib/model/agents.ts +++ b/ui/admin/app/lib/model/agents.ts @@ -1,3 +1,4 @@ +import { EnvVariable } from "~/lib/model/environmentVariables"; import { EntityMeta } from "~/lib/model/primitives"; // TODO: implement as zod schemas??? @@ -20,6 +21,7 @@ export type AgentBase = { params?: Record; knowledgeDescription?: string; model?: string; + env?: EnvVariable[]; }; export type AgentOAuthStatus = { diff --git a/ui/admin/app/lib/model/environmentVariables.ts b/ui/admin/app/lib/model/environmentVariables.ts new file mode 100644 index 00000000..c9ce65c4 --- /dev/null +++ b/ui/admin/app/lib/model/environmentVariables.ts @@ -0,0 +1,6 @@ +export type RevealedEnv = Record; + +export type EnvVariable = { + name: string; + value: string; +}; diff --git a/ui/admin/app/lib/model/workflows.ts b/ui/admin/app/lib/model/workflows.ts index dbda0d00..19d8f2d7 100644 --- a/ui/admin/app/lib/model/workflows.ts +++ b/ui/admin/app/lib/model/workflows.ts @@ -1,16 +1,9 @@ import { AgentBase } from "~/lib/model/agents"; import { EntityMeta } from "~/lib/model/primitives"; -export type WorkflowEnv = { - name: string; - value: string; - description: string; -}; - export type WorkflowBase = AgentBase & { steps: Step[]; output: string; - env?: WorkflowEnv[]; }; export type Step = { diff --git a/ui/admin/app/lib/routers/apiRoutes.ts b/ui/admin/app/lib/routers/apiRoutes.ts index 1db79145..0171d8fc 100644 --- a/ui/admin/app/lib/routers/apiRoutes.ts +++ b/ui/admin/app/lib/routers/apiRoutes.ts @@ -112,6 +112,10 @@ export const ApiRoutes = { authenticate: (workflowId: string) => buildUrl(`/workflows/${workflowId}/authenticate`), }, + env: { + getEnv: (entityId: string) => buildUrl(`/agents/${entityId}/env`), + updateEnv: (entityId: string) => buildUrl(`/agents/${entityId}/env`), + }, threads: { base: () => buildUrl("/threads"), getById: (threadId: string) => buildUrl(`/threads/${threadId}`), diff --git a/ui/admin/app/lib/service/api/EnvironmentApiService.tsx b/ui/admin/app/lib/service/api/EnvironmentApiService.tsx new file mode 100644 index 00000000..2d267c71 --- /dev/null +++ b/ui/admin/app/lib/service/api/EnvironmentApiService.tsx @@ -0,0 +1,30 @@ +import { RevealedEnv } from "~/lib/model/environmentVariables"; +import { ApiRoutes } from "~/lib/routers/apiRoutes"; +import { request } from "~/lib/service/api/primitives"; + +async function getEnvVariables(entityId: string) { + const res = await request({ + url: ApiRoutes.env.getEnv(entityId).url, + errorMessage: "Failed to fetch workflow env", + }); + + return res.data; +} + +async function updateEnvVariables(entityId: string, env: RevealedEnv) { + const payload = Object.fromEntries( + Object.entries(env).filter(([name]) => !!name) + ); + + await request({ + url: ApiRoutes.env.updateEnv(entityId).url, + method: "POST", + data: payload, + errorMessage: "Failed to update workflow env", + }); +} + +export const EnvironmentApiService = { + getEnvVariables, + updateEnvVariables, +};