Skip to content

Commit

Permalink
feat: add credential inputs to workflow form in admin ui (#716)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhopperlowe authored Dec 2, 2024
1 parent 5772b0a commit e149f0c
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 85 deletions.
6 changes: 2 additions & 4 deletions ui/admin/app/components/chat/RunWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type RunWorkflowProps = {
export function RunWorkflow({
workflowId,
onSubmit,
popoverContentProps,
...props
}: RunWorkflowProps & ButtonProps) {
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -56,10 +57,7 @@ export function RunWorkflow({
</Button>
</PopoverTrigger>

<PopoverContent
{...props.popoverContentProps}
className="min-w-full"
>
<PopoverContent {...popoverContentProps} className="min-w-full">
<RunWorkflowForm
params={params}
onSubmit={(params) => {
Expand Down
3 changes: 2 additions & 1 deletion ui/admin/app/components/form/controlledInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ export function ControlledInput<
}}
className={cn(
getFieldStateClasses(fieldState),
className
className,
classNames.input
)}
/>
</BasicInputItem>
Expand Down
148 changes: 70 additions & 78 deletions ui/admin/app/components/workflow/StringArrayForm.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,108 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, TrashIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useEffect, useMemo } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";

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

import { ControlledInput } from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { Form, FormField, FormItem, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { Form } from "~/components/ui/form";

const formSchema = z.object({
items: z.array(z.string()),
items: z.array(z.object({ value: z.string() })),
});

export type StringArrayFormValues = z.infer<typeof formSchema>;

export function StringArrayForm({
initialItems = [],
initialItems,
onSubmit,
onChange,
itemName,
placeholder,
}: {
initialItems?: string[];
onSubmit?: (values: StringArrayFormValues) => void;
onChange?: (values: StringArrayFormValues) => void;
onSubmit?: (values: string[]) => void;
onChange?: (values: string[]) => void;
itemName: string;
placeholder: string;
}) {
const defaultValues = useMemo(
() => convertToFormValues(initialItems ?? []),
[initialItems]
);

const form = useForm<StringArrayFormValues>({
resolver: zodResolver(formSchema),
defaultValues: { items: initialItems },
defaultValues,
});

const handleSubmit = form.handleSubmit(onSubmit || noop);

const [newItem, setNewItem] = useState("");
const handleSubmit = form.handleSubmit((values) => {
if (!onSubmit) return;

const itemValues = form.watch("items");
onSubmit(convertFromFormValues(values));
});

useEffect(() => {
if (!onChange) return;

return form.watch((values) => {
const { data, success } = formSchema.safeParse(values);
if (!success) return;
onChange?.(data);
onChange(convertFromFormValues(data));
}).unsubscribe;
}, [itemValues, form.formState, onChange, form]);
}, [form.formState, onChange, form]);

const items = useFieldArray({
control: form.control,
name: "items",
});

return (
<Form {...form}>
<form onSubmit={handleSubmit}>
<FormField
control={form.control}
name="items"
render={({ field }) => (
<FormItem>
<div className="flex space-x-2">
<Input
placeholder={placeholder}
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
className="flex-grow"
/>
<Button
type="button"
variant="secondary"
size="icon"
onClick={() => {
if (newItem.trim()) {
field.onChange([
...(field.value || []),
newItem.trim(),
]);
setNewItem("");
}
}}
>
<Plus className="w-4 h-4" />
</Button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
{items.fields.map((field, index) => (
<div
key={field.id}
className="flex gap-2 p-2 rounded-md bg-muted"
>
<ControlledInput
classNames={{
wrapper: "flex-grow",
input: "bg-background",
}}
control={form.control}
name={`items.${index}.value`}
placeholder={placeholder}
/>

<Button
size="icon"
variant="ghost"
onClick={() => items.remove(index)}
startContent={<TrashIcon />}
/>
</div>
))}

<div className="mt-2 w-full">
{field.value?.map((item, index) => (
<div
key={index}
className="flex items-center space-x-2 justify-between mt-2"
>
<div className="border text-sm px-3 shadow-sm rounded-md p-2 w-full truncate">
{item}
</div>
<Button
type="button"
variant="destructive"
size="icon"
onClick={() => {
const newItems =
field.value?.filter(
(_, i) => i !== index
);
field.onChange(newItems);
}}
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
className="self-end"
variant="ghost"
onClick={() => items.append({ value: "" })}
startContent={<PlusIcon />}
>
Add {itemName}
</Button>
</form>
</Form>
);
}

function convertToFormValues(items: string[]) {
return { items: items.map((item) => ({ value: item })) };
}

function convertFromFormValues(values: StringArrayFormValues) {
return values.items.map((item) => item.value);
}
26 changes: 25 additions & 1 deletion ui/admin/app/components/workflow/Workflow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Library, List, PuzzleIcon, Variable, WrenchIcon } from "lucide-react";
import {
KeyIcon,
Library,
List,
PuzzleIcon,
Variable,
WrenchIcon,
} from "lucide-react";
import { useCallback, useState } from "react";

import { Workflow as WorkflowType } from "~/lib/model/workflows";
Expand All @@ -11,6 +18,7 @@ import { BasicToolForm } from "~/components/tools/BasicToolForm";
import { CardDescription } from "~/components/ui/card";
import { ScrollArea } from "~/components/ui/scroll-area";
import { ParamsForm } from "~/components/workflow/ParamsForm";
import { StringArrayForm } from "~/components/workflow/StringArrayForm";
import {
WorkflowProvider,
useWorkflow,
Expand Down Expand Up @@ -82,6 +90,22 @@ function WorkflowContent({ className }: WorkflowProps) {
/>
</div>

<div className="p-4 m-4 flex flex-col gap-4">
<TypographyH4 className="flex items-center gap-2">
<KeyIcon className="w-4 h-4" />
Credentials
</TypographyH4>

<StringArrayForm
initialItems={workflow.credentials}
onChange={(values) =>
debouncedSetWorkflowInfo({ credentials: values })
}
itemName="Credential"
placeholder="Enter a credential"
/>
</div>

<div className="p-4 m-4 flex flex-col gap-4">
<TypographyH4 className="flex items-center gap-2">
<Variable className="w-4 h-4" />
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/app/lib/model/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type WorkflowBase = AgentBase & {
steps: Step[];
output: string;
env?: WorkflowEnv[];
credentials: string[];
credentials?: string[];
};

export type Step = {
Expand Down

0 comments on commit e149f0c

Please sign in to comment.