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 { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel";
import {
dataSchemaExampleValue,
@@ -35,17 +34,11 @@ import {
fieldPlaceholders,
helpTooltipContent,
type TaskNode,
type TaskNodeDisplayMode,
} from "./types";
import { useParams } from "react-router-dom";
function getLocalStorageKey(workflowPermanentId: string, label: string) {
return `skyvern-task-block-${workflowPermanentId}-${label}`;
}
import { Separator } from "@/components/ui/separator";
function TaskNode({ id, data }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow();
const { workflowPermanentId } = useParams();
const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback();
const nodes = useNodes<AppNode>();
@@ -56,15 +49,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
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({
url: data.url,
navigationGoal: data.navigationGoal,
@@ -74,6 +58,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
maxStepsOverride: data.maxStepsOverride,
allowDownloads: data.allowDownloads,
continueOnFailure: data.continueOnFailure,
cacheActions: data.cacheActions,
downloadSuffix: data.downloadSuffix,
errorCodeMapping: data.errorCodeMapping,
totpVerificationUrl: data.totpVerificationUrl,
@@ -88,386 +73,6 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
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 (
<div>
<Handle
@@ -482,7 +87,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
id="b"
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 gap-2">
<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>
<TaskNodeDisplayModeSwitch
value={displayMode}
onChange={(mode) => {
setDisplayMode(mode);
if (workflowPermanentId) {
localStorage.setItem(
getLocalStorageKey(workflowPermanentId, label),
mode,
);
}
}}
/>
{displayMode === "basic" && basicContent}
{displayMode === "advanced" && advancedContent}
<Accordion type="multiple" defaultValue={["content", "extraction"]}>
<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="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>
);

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.",
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.",
continueOnFailure:
"Allow the workflow to continue if it encounters a failure.",
cacheActions: "Cache the actions of this task.",
} as const;
export const fieldPlaceholders = {