Improve run workflow form (#1032)

This commit is contained in:
Shuchang Zheng
2024-10-23 07:12:36 -07:00
committed by GitHub
parent 96c807e949
commit 426acbc3e5
5 changed files with 155 additions and 162 deletions

View File

@@ -1,5 +1,11 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -8,14 +14,6 @@ import { WorkflowParameterInput } from "./WorkflowParameterInput";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToastAction } from "@radix-ui/react-toast"; import { ToastAction } from "@radix-ui/react-toast";
import fetchToCurl from "fetch-to-curl"; import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl } from "@/util/env"; import { apiBaseUrl } from "@/util/env";
@@ -115,133 +113,122 @@ function RunWorkflowForm({ workflowParameters, initialValues }: Props) {
} }
return ( return (
<div> <Form {...form}>
<Form {...form}> <form
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> onSubmit={form.handleSubmit(onSubmit)}
<Table> className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5"
<TableHeader className="bg-slate-elevation2 text-slate-400 [&_tr]:border-b-0"> >
<TableRow className="rounded-lg px-6 [&_th:first-child]:pl-6 [&_th]:py-4"> {workflowParameters?.map((parameter) => {
<TableHead className="w-1/3 text-sm text-slate-400"> return (
Parameter Name <FormField
</TableHead> key={parameter.key}
<TableHead className="w-1/3 text-sm text-slate-400"> control={form.control}
Description name={parameter.key}
</TableHead> rules={{
<TableHead className="w-1/3 text-sm text-slate-400"> validate: (value) => {
Input if (
</TableHead> parameter.workflow_parameter_type === "json" &&
</TableRow> typeof value === "string"
</TableHeader> ) {
<TableBody> try {
{workflowParameters?.map((parameter) => { JSON.parse(value);
return ( return true;
<FormField } catch (e) {
key={parameter.key} return "Invalid JSON";
control={form.control} }
name={parameter.key} }
rules={{ if (value === null) {
validate: (value) => { return "This field is required";
if ( }
parameter.workflow_parameter_type === "json" && },
typeof value === "string"
) {
try {
JSON.parse(value);
return true;
} catch (e) {
return "Invalid JSON";
}
}
if (value === null) {
return "This field is required";
}
},
}}
render={({ field }) => {
return (
<TableRow className="[&_td:first-child]:pl-6 [&_td:last-child]:pr-6 [&_td]:py-4">
<TableCell className="w-1/3">
<div className="flex h-8 w-fit items-center rounded-sm bg-slate-elevation3 p-3">
{parameter.key}
</div>
</TableCell>
<TableCell className="w-1/3">
<div>{parameter.description}</div>
</TableCell>
<TableCell className="w-1/3">
<FormItem>
<FormControl>
<WorkflowParameterInput
type={parameter.workflow_parameter_type}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
{form.formState.errors[parameter.key] && (
<div className="text-destructive">
{
form.formState.errors[parameter.key]
?.message
}
</div>
)}
</FormItem>
</TableCell>
</TableRow>
);
}}
/>
);
})}
</TableBody>
</Table>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="secondary"
onClick={() => {
const parsedValues = parseValuesForWorkflowRun(
form.getValues(),
workflowParameters,
);
const curl = fetchToCurl({
method: "POST",
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
body: {
data: parsedValues,
proxy_location: "RESIDENTIAL",
},
headers: {
"Content-Type": "application/json",
"x-api-key": apiCredential ?? "<your-api-key>",
},
});
copyText(curl).then(() => {
toast({
variant: "success",
title: "Copied to Clipboard",
description:
"The cURL command has been copied to your clipboard.",
});
});
}} }}
> render={({ field }) => {
<CopyIcon className="mr-2 h-4 w-4" /> return (
cURL <FormItem>
</Button> <div className="flex gap-16">
<Button type="submit" disabled={runWorkflowMutation.isPending}> <FormLabel>
{runWorkflowMutation.isPending && ( <div className="w-72">
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> <div className="flex items-center gap-2 text-lg">
)} {parameter.key}
{!runWorkflowMutation.isPending && ( <span className="text-sm text-slate-400">
<PlayIcon className="mr-2 h-4 w-4" /> {parameter.workflow_parameter_type}
)} </span>
Run workflow </div>
</Button> <h2 className="text-sm text-slate-400">
</div> {parameter.description}
</form> </h2>
</Form> </div>
</div> </FormLabel>
<div className="w-full space-y-2">
<FormControl>
<WorkflowParameterInput
type={parameter.workflow_parameter_type}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
{form.formState.errors[parameter.key] && (
<div className="text-destructive">
{form.formState.errors[parameter.key]?.message}
</div>
)}
</div>
</div>
</FormItem>
);
}}
/>
);
})}
{workflowParameters.length === 0 && (
<div>No workflow parameters for this workflow.</div>
)}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="secondary"
onClick={() => {
const parsedValues = parseValuesForWorkflowRun(
form.getValues(),
workflowParameters,
);
const curl = fetchToCurl({
method: "POST",
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
body: {
data: parsedValues,
proxy_location: "RESIDENTIAL",
},
headers: {
"Content-Type": "application/json",
"x-api-key": apiCredential ?? "<your-api-key>",
},
});
copyText(curl).then(() => {
toast({
variant: "success",
title: "Copied to Clipboard",
description:
"The cURL command has been copied to your clipboard.",
});
});
}}
>
<CopyIcon className="mr-2 h-4 w-4" />
cURL
</Button>
<Button type="submit" disabled={runWorkflowMutation.isPending}>
{runWorkflowMutation.isPending && (
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
)}
{!runWorkflowMutation.isPending && (
<PlayIcon className="mr-2 h-4 w-4" />
)}
Run workflow
</Button>
</div>
</form>
</Form>
); );
} }

View File

@@ -16,11 +16,14 @@ function WorkflowParameterInput({ type, value, onChange }: Props) {
if (type === "json") { if (type === "json") {
return ( return (
<CodeEditor <CodeEditor
className="w-full"
language="json" language="json"
onChange={(value) => onChange(value)} onChange={(value) => onChange(value)}
value={ value={
typeof value === "string" ? value : JSON.stringify(value, null, 2) typeof value === "string" ? value : JSON.stringify(value, null, 2)
} }
minHeight="96px"
maxHeight="500px"
/> />
); );
} }
@@ -37,7 +40,7 @@ function WorkflowParameterInput({ type, value, onChange }: Props) {
if (type === "integer") { if (type === "integer") {
return ( return (
<Input <Input
value={value as number} value={value === null ? "" : Number(value)}
onChange={(e) => onChange(parseInt(e.target.value))} onChange={(e) => onChange(parseInt(e.target.value))}
type="number" type="number"
/> />
@@ -47,7 +50,7 @@ function WorkflowParameterInput({ type, value, onChange }: Props) {
if (type === "float") { if (type === "float") {
return ( return (
<Input <Input
value={value as number} value={value === null ? "" : Number(value)}
onChange={(e) => onChange(parseFloat(e.target.value))} onChange={(e) => onChange(parseFloat(e.target.value))}
type="number" type="number"
step="any" step="any"

View File

@@ -3,27 +3,8 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useLocation, useParams } from "react-router-dom"; import { useLocation, useParams } from "react-router-dom";
import { RunWorkflowForm } from "./RunWorkflowForm"; import { RunWorkflowForm } from "./RunWorkflowForm";
import { import { WorkflowApiResponse } from "./types/workflowTypes";
WorkflowApiResponse, import { Skeleton } from "@/components/ui/skeleton";
WorkflowParameterValueType,
} from "./types/workflowTypes";
function defaultValue(type: WorkflowParameterValueType) {
switch (type) {
case "string":
return "";
case "integer":
return 0;
case "float":
return 0.0;
case "boolean":
return false;
case "json":
return null;
case "file_url":
return null;
}
}
function WorkflowRunParameters() { function WorkflowRunParameters() {
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
@@ -65,18 +46,36 @@ function WorkflowRunParameters() {
acc[curr.key] = Boolean(curr.default_value); acc[curr.key] = Boolean(curr.default_value);
return acc; return acc;
} }
if (
curr.default_value === null &&
curr.workflow_parameter_type === "string"
) {
acc[curr.key] = "";
return acc;
}
if (curr.default_value) { if (curr.default_value) {
acc[curr.key] = curr.default_value; acc[curr.key] = curr.default_value;
return acc; return acc;
} }
acc[curr.key] = defaultValue(curr.workflow_parameter_type); acc[curr.key] = null;
return acc; return acc;
}, },
{} as Record<string, unknown>, {} as Record<string, unknown>,
); );
if (isFetching) { if (isFetching) {
return <div>Getting workflow parameters...</div>; return (
<div className="space-y-8">
<header className="space-y-5">
<h1 className="text-3xl">Run Parameters</h1>
<h2 className="text-lg text-slate-400">
Fill the placeholder values that you have linked throughout your
workflow.
</h2>
</header>
<Skeleton className="h-96 w-full" />
</div>
);
} }
if (!workflow || !workflowParameters || !initialValues) { if (!workflow || !workflowParameters || !initialValues) {

View File

@@ -220,7 +220,9 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
parameterType: "workflow", parameterType: "workflow",
dataType: parameterType, dataType: parameterType,
description, description,
defaultValue: defaultValue, defaultValue: defaultValueState.hasDefaultValue
? defaultValue
: null,
}); });
} }
if (type === "credential") { if (type === "credential") {

View File

@@ -253,7 +253,9 @@ function WorkflowParameterEditPanel({
parameterType: "workflow", parameterType: "workflow",
dataType: parameterType, dataType: parameterType,
description, description,
defaultValue: defaultValue, defaultValue: defaultValueState.hasDefaultValue
? defaultValue
: null,
}); });
} }
if (type === "credential") { if (type === "credential") {