Skip to content

Commit

Permalink
Chore/admin/update-environment-variables (#954)
Browse files Browse the repository at this point in the history
* enhance: move workflow env variable form to dialog

Signed-off-by: Ryan Hopper-Lowe <[email protected]>

* 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 <[email protected]>
  • Loading branch information
ryanhopperlowe authored Dec 18, 2024
1 parent 4deac11 commit b811d48
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 78 deletions.
16 changes: 15 additions & 1 deletion ui/admin/app/components/agent/Agent.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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";
Expand Down Expand Up @@ -125,6 +126,19 @@ export function Agent({ className, onRefresh }: AgentProps) {
/>
</div>

<div className="p-4 m-4 space-y-4 lg:mx-6 xl:mx-8">
<TypographyH4 className="flex items-center gap-2 border-b pb-2">
<VariableIcon className="w-5 h-5" />
Environment Variables
</TypographyH4>

<EnvironmentVariableSection
entity={agent}
onUpdate={partialSetAgent}
entityType="agent"
/>
</div>

<div className="p-4 m-4 space-y-4 lg:mx-6 xl:mx-8">
<TypographyH4 className="flex items-center gap-2 border-b pb-2">
<LibraryIcon className="w-6 h-6" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLFormElement>) => {
e.preventDefault();

if (defaultValues) {
const updates = Object.fromEntries(
state.map(({ name, description }) => [name, description])
);

updateEnv(updates);
}
};

return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<NameDescriptionForm
defaultValues={state}
onChange={setState}
descriptionFieldProps={{
type: "password",
placeholder: "Value",
}}
/>

<Button className="w-full" type="submit" loading={isLoading}>
Save
</Button>
</form>
);
}
118 changes: 118 additions & 0 deletions ui/admin/app/components/agent/shared/EnvironmentVariableSection.tsx
Original file line number Diff line number Diff line change
@@ -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<Agent | Workflow>) => 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 (
<div className="flex flex-col gap-2">
<Card className="py-2 px-4">
<SelectList
getItemKey={(item) => item.name}
items={items}
renderItem={renderItem}
selected={items.map((item) => item.name)}
/>
</Card>

<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
variant="ghost"
loading={revealEnv.isLoading}
className="self-end"
startContent={<PenIcon />}
>
Environment Variables
</Button>
</DialogTrigger>

<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Environment Variables</DialogTitle>
</DialogHeader>

<DialogDescription>
Environment variables are used to store values that can
be used in your {entityType}.
</DialogDescription>

{revealEnv.data && (
<EnvForm
defaultValues={revealEnv.data}
isLoading={updateEnv.isLoading}
onSubmit={(values) =>
updateEnv.execute(entity.id, values)
}
/>
)}
</DialogContent>
</Dialog>
</div>
);

function renderItem(item: EnvVariable) {
return (
<div className="flex items-center justify-between gap-2 w-full">
<TypographyP className="flex-1">{item.name}</TypographyP>
<TypographyP>{"•".repeat(15)}</TypographyP>
</div>
);
}
}
6 changes: 4 additions & 2 deletions ui/admin/app/components/composed/NameDescriptionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,25 @@ export function NameDescriptionForm({
key={field.id}
>
<ControlledInput
placeholder="Name"
{...nameFieldProps}
control={form.control}
name={`params.${i}.name`}
placeholder="Name"
classNames={{ wrapper: "flex-auto bg-background" }}
/>

<ControlledInput
placeholder="Description"
{...descriptionFieldProps}
control={form.control}
name={`params.${i}.description`}
placeholder="Description"
classNames={{ wrapper: "flex-auto bg-background" }}
/>

<Button
variant="ghost"
size="icon"
type="button"
onClick={() => paramFields.remove(i)}
>
<TrashIcon />
Expand All @@ -97,6 +98,7 @@ export function NameDescriptionForm({
variant="ghost"
className="self-end"
startContent={<PlusIcon />}
type="button"
onClick={() =>
paramFields.append({ name: "", description: "" })
}
Expand Down
42 changes: 31 additions & 11 deletions ui/admin/app/components/composed/SelectModule.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import { useMemo } from "react";

import { cn } from "~/lib/utils";

import { Button } from "~/components/ui/button";
import {
Command,
Expand Down Expand Up @@ -138,10 +140,15 @@ export function SelectPopover<T>({
interface SelectListProps<T> {
selected: string[];
items?: T[];
onRemove: (id: string) => void;
onRemove?: (id: string) => void;
renderItem: (item: T) => React.ReactNode;
fallbackRender?: (id: string) => React.ReactNode;
getItemKey: (item: T) => string;
classNames?: {
container?: string;
item?: string;
remove?: string;
};
}

export function SelectList<T>({
Expand All @@ -151,6 +158,7 @@ export function SelectList<T>({
renderItem,
fallbackRender = (id) => id,
getItemKey,
classNames,
}: SelectListProps<T>) {
const itemMap = useMemo(() => {
return items.reduce(
Expand All @@ -163,21 +171,33 @@ export function SelectList<T>({
}, [items, getItemKey]);

return (
<div className="flex flex-col gap-2 divide-y">
{selected.map((id) => (
<div
className={cn(
"flex flex-col gap-2 divide-y",
classNames?.container
)}
>
{selected.map((id, i) => (
<div
key={id}
className="flex items-center justify-between gap-2 pt-2"
className={cn(
"flex items-center justify-between gap-2 pt-1",
i > 0 && "pt-2",
classNames?.item
)}
>
{itemMap[id] ? renderItem(itemMap[id]) : fallbackRender(id)}

<Button
variant="ghost"
size="icon"
onClick={() => onRemove(id)}
>
<TrashIcon />
</Button>
{onRemove && (
<Button
variant="ghost"
size="icon"
onClick={() => onRemove(id)}
className={classNames?.remove}
>
<TrashIcon />
</Button>
)}
</div>
))}
</div>
Expand Down
Loading

0 comments on commit b811d48

Please sign in to comment.