Improve run workflow form (#1032)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user