Task block changes (#989)

This commit is contained in:
Shuchang Zheng
2024-10-16 12:45:06 -07:00
committed by GitHub
parent a988fe9410
commit a239030830
3 changed files with 354 additions and 448 deletions

View File

@@ -27,7 +27,6 @@ import { AppNode } from "..";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel"; import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel";
import { import {
dataSchemaExampleValue, dataSchemaExampleValue,
@@ -35,17 +34,11 @@ import {
fieldPlaceholders, fieldPlaceholders,
helpTooltipContent, helpTooltipContent,
type TaskNode, type TaskNode,
type TaskNodeDisplayMode,
} from "./types"; } from "./types";
import { useParams } from "react-router-dom"; import { Separator } from "@/components/ui/separator";
function getLocalStorageKey(workflowPermanentId: string, label: string) {
return `skyvern-task-block-${workflowPermanentId}-${label}`;
}
function TaskNode({ id, data }: NodeProps<TaskNode>) { function TaskNode({ id, data }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
const { workflowPermanentId } = useParams();
const { editable } = data; const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const nodes = useNodes<AppNode>(); const nodes = useNodes<AppNode>();
@@ -56,15 +49,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
initialValue: data.label, initialValue: data.label,
}); });
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>(
workflowPermanentId &&
localStorage.getItem(getLocalStorageKey(workflowPermanentId, label))
? (localStorage.getItem(
getLocalStorageKey(workflowPermanentId, label),
) as TaskNodeDisplayMode)
: "basic",
);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
url: data.url, url: data.url,
navigationGoal: data.navigationGoal, navigationGoal: data.navigationGoal,
@@ -74,6 +58,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
maxStepsOverride: data.maxStepsOverride, maxStepsOverride: data.maxStepsOverride,
allowDownloads: data.allowDownloads, allowDownloads: data.allowDownloads,
continueOnFailure: data.continueOnFailure, continueOnFailure: data.continueOnFailure,
cacheActions: data.cacheActions,
downloadSuffix: data.downloadSuffix, downloadSuffix: data.downloadSuffix,
errorCodeMapping: data.errorCodeMapping, errorCodeMapping: data.errorCodeMapping,
totpVerificationUrl: data.totpVerificationUrl, totpVerificationUrl: data.totpVerificationUrl,
@@ -88,386 +73,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
const basicContent = (
<>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={helpTooltipContent["url"]} />
</div>
<AutoResizingTextarea
value={inputs.url}
className="nopan text-xs"
name="url"
onChange={(event) => {
if (!editable) {
return;
}
handleChange("url", event.target.value);
}}
placeholder={fieldPlaceholders["url"]}
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip content={helpTooltipContent["navigationGoal"]} />
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("navigationGoal", event.target.value);
}}
value={inputs.navigationGoal}
placeholder={fieldPlaceholders["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<TaskNodeParametersPanel
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
</>
);
const advancedContent = (
<>
<Accordion type="multiple" defaultValue={["content"]}>
<AccordionItem value="content">
<AccordionTrigger>Content</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">URL</Label>
<HelpTooltip content={helpTooltipContent["url"]} />
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("url", event.target.value);
}}
value={inputs.url}
placeholder={fieldPlaceholders["url"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip content={helpTooltipContent["navigationGoal"]} />
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("navigationGoal", event.target.value);
}}
value={inputs.navigationGoal}
placeholder={fieldPlaceholders["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<TaskNodeParametersPanel
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="extraction">
<AccordionTrigger>Extraction</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Extraction Goal
</Label>
<HelpTooltip
content={helpTooltipContent["dataExtractionGoal"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("dataExtractionGoal", event.target.value);
}}
value={inputs.dataExtractionGoal}
placeholder={fieldPlaceholders["dataExtractionGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-4">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Schema
</Label>
<HelpTooltip content={helpTooltipContent["dataSchema"]} />
</div>
<Checkbox
checked={inputs.dataSchema !== "null"}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange(
"dataSchema",
checked
? JSON.stringify(dataSchemaExampleValue, null, 2)
: "null",
);
}}
/>
</div>
{inputs.dataSchema !== "null" && (
<div>
<CodeEditor
language="json"
value={inputs.dataSchema}
onChange={(value) => {
if (!editable) {
return;
}
handleChange("dataSchema", value);
}}
className="nowheel nopan"
/>
</div>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="limits">
<AccordionTrigger>Limits</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1 pt-1">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Max Retries
</Label>
<HelpTooltip content={helpTooltipContent["maxRetries"]} />
</div>
<Input
type="number"
placeholder={fieldPlaceholders["maxRetries"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxRetries ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
const value =
event.target.value === ""
? null
: Number(event.target.value);
handleChange("maxRetries", value);
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Max Steps Override
</Label>
<HelpTooltip
content={helpTooltipContent["maxStepsOverride"]}
/>
</div>
<Input
type="number"
placeholder={fieldPlaceholders["maxStepsOverride"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxStepsOverride ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
const value =
event.target.value === ""
? null
: Number(event.target.value);
handleChange("maxStepsOverride", value);
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Complete on Download
</Label>
<HelpTooltip
content={helpTooltipContent["completeOnDownload"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.allowDownloads}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("allowDownloads", checked);
}}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("continueOnFailure", checked);
}}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
File Suffix
</Label>
<HelpTooltip content={helpTooltipContent["fileSuffix"]} />
</div>
<Input
type="text"
placeholder={fieldPlaceholders["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
handleChange("downloadSuffix", event.target.value);
}}
/>
</div>
<div className="space-y-2">
<div className="flex gap-4">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Error Messages
</Label>
<HelpTooltip
content={helpTooltipContent["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(errorMappingExampleValue, null, 2)
: "null",
);
}}
/>
</div>
{inputs.errorCodeMapping !== "null" && (
<div>
<CodeEditor
language="json"
value={inputs.errorCodeMapping}
onChange={(value) => {
if (!editable) {
return;
}
handleChange("errorCodeMapping", value);
}}
className="nowheel nopan"
/>
</div>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="totp">
<AccordionTrigger>Two-Factor Authentication</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltipContent["totpVerificationUrl"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("totpVerificationUrl", event.target.value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={fieldPlaceholders["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip content={helpTooltipContent["totpIdentifier"]} />
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("totpIdentifier", event.target.value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={fieldPlaceholders["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</>
);
return ( return (
<div> <div>
<Handle <Handle
@@ -482,7 +87,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
id="b" id="b"
className="opacity-0" className="opacity-0"
/> />
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4"> <div className="w-[30rem] space-y-2 rounded-lg bg-slate-elevation3 px-6 py-4">
<div className="flex h-[2.75rem] justify-between"> <div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600"> <div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
@@ -505,20 +110,354 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
}} }}
/> />
</div> </div>
<TaskNodeDisplayModeSwitch <Accordion type="multiple" defaultValue={["content", "extraction"]}>
value={displayMode} <AccordionItem value="content">
onChange={(mode) => { <AccordionTrigger>Content</AccordionTrigger>
setDisplayMode(mode); <AccordionContent className="pl-[1.5rem] pr-1">
if (workflowPermanentId) { <div className="space-y-4">
localStorage.setItem( <div className="space-y-2">
getLocalStorageKey(workflowPermanentId, label), <div className="flex gap-2">
mode, <Label className="text-xs text-slate-300">URL</Label>
); <HelpTooltip content={helpTooltipContent["url"]} />
} </div>
}} <AutoResizingTextarea
/> onChange={(event) => {
{displayMode === "basic" && basicContent} if (!editable) {
{displayMode === "advanced" && advancedContent} return;
}
handleChange("url", event.target.value);
}}
value={inputs.url}
placeholder={fieldPlaceholders["url"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Goal</Label>
<HelpTooltip
content={helpTooltipContent["navigationGoal"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("navigationGoal", event.target.value);
}}
value={inputs.navigationGoal}
placeholder={fieldPlaceholders["navigationGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<TaskNodeParametersPanel
availableOutputParameters={outputParameterKeys}
parameters={data.parameterKeys}
onParametersChange={(parameterKeys) => {
updateNodeData(id, { parameterKeys });
}}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="extraction">
<AccordionTrigger>Extraction</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Extraction Goal
</Label>
<HelpTooltip
content={helpTooltipContent["dataExtractionGoal"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("dataExtractionGoal", event.target.value);
}}
value={inputs.dataExtractionGoal}
placeholder={fieldPlaceholders["dataExtractionGoal"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-4">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
Data Schema
</Label>
<HelpTooltip content={helpTooltipContent["dataSchema"]} />
</div>
<Checkbox
checked={inputs.dataSchema !== "null"}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange(
"dataSchema",
checked
? JSON.stringify(dataSchemaExampleValue, null, 2)
: "null",
);
}}
/>
</div>
{inputs.dataSchema !== "null" && (
<div>
<CodeEditor
language="json"
value={inputs.dataSchema}
onChange={(value) => {
if (!editable) {
return;
}
handleChange("dataSchema", value);
}}
className="nowheel nopan"
/>
</div>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger>Advanced Settings</AccordionTrigger>
<AccordionContent className="pl-[1.5rem] pr-1 pt-1">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Max Retries
</Label>
<HelpTooltip content={helpTooltipContent["maxRetries"]} />
</div>
<Input
type="number"
placeholder={fieldPlaceholders["maxRetries"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxRetries ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
const value =
event.target.value === ""
? null
: Number(event.target.value);
handleChange("maxRetries", value);
}}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Max Steps Override
</Label>
<HelpTooltip
content={helpTooltipContent["maxStepsOverride"]}
/>
</div>
<Input
type="number"
placeholder={fieldPlaceholders["maxStepsOverride"]}
className="nopan w-52 text-xs"
min="0"
value={inputs.maxStepsOverride ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
const value =
event.target.value === ""
? null
: Number(event.target.value);
handleChange("maxStepsOverride", value);
}}
/>
</div>
<div className="space-y-2">
<div className="flex gap-4">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Error Messages
</Label>
<HelpTooltip
content={helpTooltipContent["errorCodeMapping"]}
/>
</div>
<Checkbox
checked={inputs.errorCodeMapping !== "null"}
disabled={!editable}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange(
"errorCodeMapping",
checked
? JSON.stringify(errorMappingExampleValue, null, 2)
: "null",
);
}}
/>
</div>
{inputs.errorCodeMapping !== "null" && (
<div>
<CodeEditor
language="json"
value={inputs.errorCodeMapping}
onChange={(value) => {
if (!editable) {
return;
}
handleChange("errorCodeMapping", value);
}}
className="nowheel nopan"
/>
</div>
)}
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Continue on Failure
</Label>
<HelpTooltip
content={helpTooltipContent["continueOnFailure"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.continueOnFailure}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("continueOnFailure", checked);
}}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Cache Actions
</Label>
<HelpTooltip content={helpTooltipContent["cacheActions"]} />
</div>
<div className="w-52">
<Switch
checked={inputs.cacheActions}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("cacheActions", checked);
}}
/>
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Complete on Download
</Label>
<HelpTooltip
content={helpTooltipContent["completeOnDownload"]}
/>
</div>
<div className="w-52">
<Switch
checked={inputs.allowDownloads}
onCheckedChange={(checked) => {
if (!editable) {
return;
}
handleChange("allowDownloads", checked);
}}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
File Suffix
</Label>
<HelpTooltip content={helpTooltipContent["fileSuffix"]} />
</div>
<Input
type="text"
placeholder={fieldPlaceholders["downloadSuffix"]}
className="nopan w-52 text-xs"
value={inputs.downloadSuffix ?? ""}
onChange={(event) => {
if (!editable) {
return;
}
handleChange("downloadSuffix", event.target.value);
}}
/>
</div>
<Separator />
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Verification URL
</Label>
<HelpTooltip
content={helpTooltipContent["totpVerificationUrl"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("totpVerificationUrl", event.target.value);
}}
value={inputs.totpVerificationUrl ?? ""}
placeholder={fieldPlaceholders["totpVerificationUrl"]}
className="nopan text-xs"
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">
2FA Identifier
</Label>
<HelpTooltip
content={helpTooltipContent["totpIdentifier"]}
/>
</div>
<AutoResizingTextarea
onChange={(event) => {
if (!editable) {
return;
}
handleChange("totpIdentifier", event.target.value);
}}
value={inputs.totpIdentifier ?? ""}
placeholder={fieldPlaceholders["totpIdentifier"]}
className="nopan text-xs"
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> </div>
); );

View File

@@ -1,36 +0,0 @@
import { cn } from "@/util/utils";
import { TaskNodeDisplayMode } from "./types";
type Props = {
value: TaskNodeDisplayMode;
onChange: (mode: TaskNodeDisplayMode) => void;
};
function TaskNodeDisplayModeSwitch({ value, onChange }: Props) {
return (
<div className="flex w-fit gap-1 rounded-sm border border-slate-700 p-2">
<div
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
"bg-slate-700": value === "basic",
})}
onClick={() => {
onChange("basic");
}}
>
Basic
</div>
<div
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
"bg-slate-700": value === "advanced",
})}
onClick={() => {
onChange("advanced");
}}
>
Advanced
</div>
</div>
);
}
export { TaskNodeDisplayModeSwitch };

View File

@@ -74,6 +74,9 @@ export const helpTooltipContent = {
"If you have an internal system for storing TOTP codes, link the endpoint here.", "If you have an internal system for storing TOTP codes, link the endpoint here.",
totpIdentifier: totpIdentifier:
"If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.", "If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.",
continueOnFailure:
"Allow the workflow to continue if it encounters a failure.",
cacheActions: "Cache the actions of this task.",
} as const; } as const;
export const fieldPlaceholders = { export const fieldPlaceholders = {