Add validation block (#1242)
This commit is contained in:
@@ -31,7 +31,12 @@ import {
|
|||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||||
import { edgeTypes } from "./edges";
|
import { edgeTypes } from "./edges";
|
||||||
import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes";
|
import {
|
||||||
|
AppNode,
|
||||||
|
isWorkflowBlockNode,
|
||||||
|
nodeTypes,
|
||||||
|
WorkflowBlockNode,
|
||||||
|
} from "./nodes";
|
||||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
import "./reactFlowOverrideStyles.css";
|
import "./reactFlowOverrideStyles.css";
|
||||||
@@ -68,6 +73,7 @@ import { ReloadIcon } from "@radix-ui/react-icons";
|
|||||||
import { isLoopNode, LoopNode } from "./nodes/LoopNode/types";
|
import { isLoopNode, LoopNode } from "./nodes/LoopNode/types";
|
||||||
import { isTaskNode } from "./nodes/TaskNode/types";
|
import { isTaskNode } from "./nodes/TaskNode/types";
|
||||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||||
|
import { isValidationNode } from "./nodes/ValidationNode/types";
|
||||||
|
|
||||||
function convertToParametersYAML(
|
function convertToParametersYAML(
|
||||||
parameters: ParametersState,
|
parameters: ParametersState,
|
||||||
@@ -452,6 +458,14 @@ function FlowRenderer({
|
|||||||
function getWorkflowErrors(): Array<string> {
|
function getWorkflowErrors(): Array<string> {
|
||||||
const errors: Array<string> = [];
|
const errors: Array<string> = [];
|
||||||
|
|
||||||
|
const workflowBlockNodes = nodes.filter(isWorkflowBlockNode);
|
||||||
|
if (workflowBlockNodes[0]!.type === "validation") {
|
||||||
|
const label = workflowBlockNodes[0]!.data.label;
|
||||||
|
errors.push(
|
||||||
|
`${label}: Validation block can't be the first block in a workflow`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// check loop node parameters
|
// check loop node parameters
|
||||||
const loopNodes: Array<LoopNode> = nodes.filter(isLoopNode);
|
const loopNodes: Array<LoopNode> = nodes.filter(isLoopNode);
|
||||||
const emptyLoopNodes = loopNodes.filter(
|
const emptyLoopNodes = loopNodes.filter(
|
||||||
@@ -480,6 +494,23 @@ function FlowRenderer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validationNodes = nodes.filter(isValidationNode);
|
||||||
|
validationNodes.forEach((node) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(node.data.errorCodeMapping);
|
||||||
|
} catch {
|
||||||
|
errors.push(`${node.data.label}: Error messages is not valid JSON`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node.data.completeCriterion.length === 0 &&
|
||||||
|
node.data.terminateCriterion.length === 0
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`${node.data.label}: At least one of completion or termination criteria must be provided`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NodeBaseData } from "../types";
|
|||||||
|
|
||||||
export type CodeBlockNodeData = NodeBaseData & {
|
export type CodeBlockNodeData = NodeBaseData & {
|
||||||
code: string;
|
code: string;
|
||||||
editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CodeBlockNode = Node<CodeBlockNodeData, "codeBlock">;
|
export type CodeBlockNode = Node<CodeBlockNodeData, "codeBlock">;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NodeBaseData } from "../types";
|
|||||||
|
|
||||||
export type DownloadNodeData = NodeBaseData & {
|
export type DownloadNodeData = NodeBaseData & {
|
||||||
url: string;
|
url: string;
|
||||||
editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadNode = Node<DownloadNodeData, "download">;
|
export type DownloadNode = Node<DownloadNodeData, "download">;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NodeBaseData } from "../types";
|
|||||||
|
|
||||||
export type FileParserNodeData = NodeBaseData & {
|
export type FileParserNodeData = NodeBaseData & {
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NodeBaseData } from "../types";
|
|||||||
|
|
||||||
export type LoopNodeData = NodeBaseData & {
|
export type LoopNodeData = NodeBaseData & {
|
||||||
loopValue: string;
|
loopValue: string;
|
||||||
editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export type SendEmailNodeData = NodeBaseData & {
|
|||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
fileAttachments: string;
|
fileAttachments: string;
|
||||||
editable: boolean;
|
|
||||||
sender: string;
|
sender: string;
|
||||||
smtpHostSecretParameterKey?: string;
|
smtpHostSecretParameterKey?: string;
|
||||||
smtpPortSecretParameterKey?: string;
|
smtpPortSecretParameterKey?: string;
|
||||||
|
|||||||
@@ -28,14 +28,9 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
|||||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
import { NodeActionMenu } from "../NodeActionMenu";
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
||||||
import {
|
import { fieldPlaceholders, helpTooltipContent, type TaskNode } from "./types";
|
||||||
dataSchemaExampleValue,
|
|
||||||
errorMappingExampleValue,
|
|
||||||
fieldPlaceholders,
|
|
||||||
helpTooltipContent,
|
|
||||||
type TaskNode,
|
|
||||||
} from "./types";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
|
||||||
|
|
||||||
function TaskNode({ id, data }: NodeProps<TaskNode>) {
|
function TaskNode({ id, data }: NodeProps<TaskNode>) {
|
||||||
const { updateNodeData } = useReactFlow();
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type TaskNodeData = NodeBaseData & {
|
|||||||
maxStepsOverride: number | null;
|
maxStepsOverride: number | null;
|
||||||
allowDownloads: boolean;
|
allowDownloads: boolean;
|
||||||
downloadSuffix: string | null;
|
downloadSuffix: string | null;
|
||||||
editable: boolean;
|
|
||||||
parameterKeys: Array<string>;
|
parameterKeys: Array<string>;
|
||||||
totpVerificationUrl: string | null;
|
totpVerificationUrl: string | null;
|
||||||
totpIdentifier: string | null;
|
totpIdentifier: string | null;
|
||||||
@@ -88,14 +87,3 @@ export const fieldPlaceholders = {
|
|||||||
totpVerificationUrl: "Provide your 2FA endpoint",
|
totpVerificationUrl: "Provide your 2FA endpoint",
|
||||||
totpIdentifier: "Add an ID that links your TOTP to the task",
|
totpIdentifier: "Add an ID that links your TOTP to the task",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorMappingExampleValue = {
|
|
||||||
sample_invalid_credentials: "if the credentials are incorrect, terminate",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dataSchemaExampleValue = {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
sample: { type: "string" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { NodeBaseData } from "../types";
|
|||||||
export type TextPromptNodeData = NodeBaseData & {
|
export type TextPromptNodeData = NodeBaseData & {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
jsonSchema: string;
|
jsonSchema: string;
|
||||||
editable: boolean;
|
|
||||||
label: string;
|
|
||||||
parameterKeys: Array<string>;
|
parameterKeys: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
import { helpTooltipContent, type ValidationNode } from "./types";
|
||||||
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { CheckCircledIcon } from "@radix-ui/react-icons";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
|
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||||
|
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { errorMappingExampleValue } from "../types";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
function ValidationNode({ id, data }: NodeProps<ValidationNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const { editable } = data;
|
||||||
|
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||||
|
id,
|
||||||
|
initialValue: data.label,
|
||||||
|
});
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
completeCriterion: data.completeCriterion,
|
||||||
|
terminateCriterion: data.terminateCriterion,
|
||||||
|
errorCodeMapping: data.errorCodeMapping,
|
||||||
|
});
|
||||||
|
const deleteNodeCallback = useDeleteNodeCallback();
|
||||||
|
|
||||||
|
function handleChange(key: string, value: unknown) {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs({ ...inputs, [key]: value });
|
||||||
|
updateNodeData(id, { [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||||
|
<header 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">
|
||||||
|
<CheckCircledIcon className="size-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<EditableNodeTitle
|
||||||
|
value={label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={setLabel}
|
||||||
|
titleClassName="text-base"
|
||||||
|
inputClassName="text-base"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">Validation Block</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeActionMenu
|
||||||
|
onDelete={() => {
|
||||||
|
deleteNodeCallback(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-slate-300">Complete if...</Label>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("completeCriterion", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.completeCriterion}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-slate-300">Terminate if...</Label>
|
||||||
|
<AutoResizingTextarea
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange("terminateCriterion", event.target.value);
|
||||||
|
}}
|
||||||
|
value={inputs.terminateCriterion}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem value="advanced" className="border-b-0">
|
||||||
|
<AccordionTrigger className="py-0">
|
||||||
|
Advanced Settings
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="ml-6 mt-4 space-y-4">
|
||||||
|
<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"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</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={data.continueOnFailure}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { continueOnFailure: checked });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ValidationNode };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Node } from "@xyflow/react";
|
||||||
|
import { NodeBaseData } from "../types";
|
||||||
|
|
||||||
|
export type ValidationNodeData = NodeBaseData & {
|
||||||
|
completeCriterion: string;
|
||||||
|
terminateCriterion: string;
|
||||||
|
errorCodeMapping: string;
|
||||||
|
parameterKeys: Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidationNode = Node<ValidationNodeData, "validation">;
|
||||||
|
|
||||||
|
export const validationNodeDefaultData: ValidationNodeData = {
|
||||||
|
label: "",
|
||||||
|
completeCriterion: "",
|
||||||
|
terminateCriterion: "",
|
||||||
|
errorCodeMapping: "null",
|
||||||
|
continueOnFailure: false,
|
||||||
|
editable: true,
|
||||||
|
parameterKeys: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const helpTooltipContent = {
|
||||||
|
errorCodeMapping:
|
||||||
|
"Knowing about why a block terminated can be important, specify error messages here.",
|
||||||
|
continueOnFailure:
|
||||||
|
"Allow the workflow to continue if it encounters a failure.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function isValidationNode(node: Node): node is ValidationNode {
|
||||||
|
return node.type === "validation";
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ import type { NodeAdderNode } from "./NodeAdderNode/types";
|
|||||||
import { NodeAdderNode as NodeAdderNodeComponent } from "./NodeAdderNode/NodeAdderNode";
|
import { NodeAdderNode as NodeAdderNodeComponent } from "./NodeAdderNode/NodeAdderNode";
|
||||||
import { StartNode as StartNodeComponent } from "./StartNode/StartNode";
|
import { StartNode as StartNodeComponent } from "./StartNode/StartNode";
|
||||||
import type { StartNode } from "./StartNode/types";
|
import type { StartNode } from "./StartNode/types";
|
||||||
|
import type { ValidationNode } from "./ValidationNode/types";
|
||||||
|
import { ValidationNode as ValidationNodeComponent } from "./ValidationNode/ValidationNode";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -30,7 +32,8 @@ export type WorkflowBlockNode =
|
|||||||
| CodeBlockNode
|
| CodeBlockNode
|
||||||
| FileParserNode
|
| FileParserNode
|
||||||
| UploadNode
|
| UploadNode
|
||||||
| DownloadNode;
|
| DownloadNode
|
||||||
|
| ValidationNode;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -53,4 +56,5 @@ export const nodeTypes = {
|
|||||||
download: memo(DownloadNodeComponent),
|
download: memo(DownloadNodeComponent),
|
||||||
nodeAdder: memo(NodeAdderNodeComponent),
|
nodeAdder: memo(NodeAdderNodeComponent),
|
||||||
start: memo(StartNodeComponent),
|
start: memo(StartNodeComponent),
|
||||||
|
validation: memo(ValidationNodeComponent),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
export type NodeBaseData = {
|
export type NodeBaseData = {
|
||||||
label: string;
|
label: string;
|
||||||
continueOnFailure: boolean;
|
continueOnFailure: boolean;
|
||||||
|
editable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const errorMappingExampleValue = {
|
||||||
|
sample_invalid_credentials: "if the credentials are incorrect, terminate",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const dataSchemaExampleValue = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sample: { type: "string" },
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
import {
|
import {
|
||||||
|
CheckCircledIcon,
|
||||||
Cross2Icon,
|
Cross2Icon,
|
||||||
CursorTextIcon,
|
CursorTextIcon,
|
||||||
EnvelopeClosedIcon,
|
EnvelopeClosedIcon,
|
||||||
@@ -20,54 +21,60 @@ const nodeLibraryItems: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
nodeType: "task",
|
nodeType: "task",
|
||||||
icon: <ListBulletIcon className="h-6 w-6" />,
|
icon: <ListBulletIcon className="size-6" />,
|
||||||
title: "Task Block",
|
title: "Task Block",
|
||||||
description: "Takes actions or extracts information",
|
description: "Takes actions or extracts information",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeType: "loop",
|
nodeType: "loop",
|
||||||
icon: <UpdateIcon className="h-6 w-6" />,
|
icon: <UpdateIcon className="size-6" />,
|
||||||
title: "For Loop Block",
|
title: "For Loop Block",
|
||||||
description: "Repeats nested elements",
|
description: "Repeats nested elements",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeType: "textPrompt",
|
nodeType: "textPrompt",
|
||||||
icon: <CursorTextIcon className="h-6 w-6" />,
|
icon: <CursorTextIcon className="size-6" />,
|
||||||
title: "Text Prompt Block",
|
title: "Text Prompt Block",
|
||||||
description: "Generates AI response",
|
description: "Generates AI response",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeType: "sendEmail",
|
nodeType: "sendEmail",
|
||||||
icon: <EnvelopeClosedIcon className="h-6 w-6" />,
|
icon: <EnvelopeClosedIcon className="size-6" />,
|
||||||
title: "Send Email Block",
|
title: "Send Email Block",
|
||||||
description: "Sends an email",
|
description: "Sends an email",
|
||||||
},
|
},
|
||||||
// temporarily removed
|
// temporarily removed
|
||||||
// {
|
// {
|
||||||
// nodeType: "codeBlock",
|
// nodeType: "codeBlock",
|
||||||
// icon: <CodeIcon className="h-6 w-6" />,
|
// icon: <CodeIcon className="size-6" />,
|
||||||
// title: "Code Block",
|
// title: "Code Block",
|
||||||
// description: "Executes Python code",
|
// description: "Executes Python code",
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
nodeType: "fileParser",
|
nodeType: "fileParser",
|
||||||
icon: <FileIcon className="h-6 w-6" />,
|
icon: <FileIcon className="size-6" />,
|
||||||
title: "File Parser Block",
|
title: "File Parser Block",
|
||||||
description: "Downloads and parses a file",
|
description: "Downloads and parses a file",
|
||||||
},
|
},
|
||||||
// disabled
|
// disabled
|
||||||
// {
|
// {
|
||||||
// nodeType: "download",
|
// nodeType: "download",
|
||||||
// icon: <DownloadIcon className="h-6 w-6" />,
|
// icon: <DownloadIcon className="size-6" />,
|
||||||
// title: "Download Block",
|
// title: "Download Block",
|
||||||
// description: "Downloads a file from S3",
|
// description: "Downloads a file from S3",
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
nodeType: "upload",
|
nodeType: "upload",
|
||||||
icon: <UploadIcon className="h-6 w-6" />,
|
icon: <UploadIcon className="size-6" />,
|
||||||
title: "Upload Block",
|
title: "Upload Block",
|
||||||
description: "Uploads a file to S3",
|
description: "Uploads a file to S3",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nodeType: "validation",
|
||||||
|
icon: <CheckCircledIcon className="size-6" />,
|
||||||
|
title: "Validation Block",
|
||||||
|
description: "Validate the state of the workflow or terminate",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -91,7 +98,7 @@ function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) {
|
|||||||
<h1 className="text-lg">Node Library</h1>
|
<h1 className="text-lg">Node Library</h1>
|
||||||
{!first && (
|
{!first && (
|
||||||
<Cross2Icon
|
<Cross2Icon
|
||||||
className="h-6 w-6 cursor-pointer"
|
className="size-6 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeWorkflowPanel();
|
closeWorkflowPanel();
|
||||||
}}
|
}}
|
||||||
@@ -139,7 +146,7 @@ function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PlusIcon className="h-6 w-6" />
|
<PlusIcon className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
TaskBlockYAML,
|
TaskBlockYAML,
|
||||||
TextPromptBlockYAML,
|
TextPromptBlockYAML,
|
||||||
UploadToS3BlockYAML,
|
UploadToS3BlockYAML,
|
||||||
|
ValidationBlockYAML,
|
||||||
WorkflowCreateYAMLRequest,
|
WorkflowCreateYAMLRequest,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +49,7 @@ import { taskNodeDefaultData } from "./nodes/TaskNode/types";
|
|||||||
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
||||||
import { NodeBaseData } from "./nodes/types";
|
import { NodeBaseData } from "./nodes/types";
|
||||||
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||||
|
import { validationNodeDefaultData } from "./nodes/ValidationNode/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||||
|
|
||||||
@@ -133,6 +135,7 @@ function convertToNode(
|
|||||||
const commonData: NodeBaseData = {
|
const commonData: NodeBaseData = {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
continueOnFailure: block.continue_on_failure,
|
continueOnFailure: block.continue_on_failure,
|
||||||
|
editable: true,
|
||||||
};
|
};
|
||||||
switch (block.block_type) {
|
switch (block.block_type) {
|
||||||
case "task": {
|
case "task": {
|
||||||
@@ -142,7 +145,6 @@ function convertToNode(
|
|||||||
type: "task",
|
type: "task",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
url: block.url ?? "",
|
url: block.url ?? "",
|
||||||
navigationGoal: block.navigation_goal ?? "",
|
navigationGoal: block.navigation_goal ?? "",
|
||||||
dataExtractionGoal: block.data_extraction_goal ?? "",
|
dataExtractionGoal: block.data_extraction_goal ?? "",
|
||||||
@@ -159,6 +161,20 @@ function convertToNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "validation": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "validation",
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||||
|
completeCriterion: block.complete_criterion ?? "",
|
||||||
|
terminateCriterion: block.terminate_criterion ?? "",
|
||||||
|
parameterKeys: block.parameters.map((p) => p.key),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "code": {
|
case "code": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -166,7 +182,6 @@ function convertToNode(
|
|||||||
type: "codeBlock",
|
type: "codeBlock",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
code: block.code,
|
code: block.code,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -178,7 +193,6 @@ function convertToNode(
|
|||||||
type: "sendEmail",
|
type: "sendEmail",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
body: block.body,
|
body: block.body,
|
||||||
fileAttachments: block.file_attachments.join(", "),
|
fileAttachments: block.file_attachments.join(", "),
|
||||||
recipients: block.recipients.join(", "),
|
recipients: block.recipients.join(", "),
|
||||||
@@ -198,7 +212,6 @@ function convertToNode(
|
|||||||
type: "textPrompt",
|
type: "textPrompt",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
prompt: block.prompt,
|
prompt: block.prompt,
|
||||||
jsonSchema: JSON.stringify(block.json_schema, null, 2),
|
jsonSchema: JSON.stringify(block.json_schema, null, 2),
|
||||||
parameterKeys: block.parameters.map((p) => p.key),
|
parameterKeys: block.parameters.map((p) => p.key),
|
||||||
@@ -212,7 +225,6 @@ function convertToNode(
|
|||||||
type: "loop",
|
type: "loop",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
loopValue: block.loop_over.key,
|
loopValue: block.loop_over.key,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -224,7 +236,6 @@ function convertToNode(
|
|||||||
type: "fileParser",
|
type: "fileParser",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
fileUrl: block.file_url,
|
fileUrl: block.file_url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -237,7 +248,6 @@ function convertToNode(
|
|||||||
type: "download",
|
type: "download",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
url: block.url,
|
url: block.url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -250,7 +260,6 @@ function convertToNode(
|
|||||||
type: "upload",
|
type: "upload",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
editable: true,
|
|
||||||
path: block.path,
|
path: block.path,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -456,6 +465,17 @@ function createNode(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "validation": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "validation",
|
||||||
|
data: {
|
||||||
|
...validationNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case "loop": {
|
case "loop": {
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
@@ -575,6 +595,19 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
|||||||
cache_actions: node.data.cacheActions,
|
cache_actions: node.data.cacheActions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "validation": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "validation",
|
||||||
|
complete_criterion: node.data.completeCriterion,
|
||||||
|
terminate_criterion: node.data.terminateCriterion,
|
||||||
|
parameter_keys: node.data.parameterKeys,
|
||||||
|
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
> | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "sendEmail": {
|
case "sendEmail": {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -1045,6 +1078,17 @@ function convertBlocks(blocks: Array<WorkflowBlock>): Array<BlockYAML> {
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
return blockYaml;
|
||||||
}
|
}
|
||||||
|
case "validation": {
|
||||||
|
const blockYaml: ValidationBlockYAML = {
|
||||||
|
...base,
|
||||||
|
block_type: "validation",
|
||||||
|
complete_criterion: block.complete_criterion,
|
||||||
|
terminate_criterion: block.terminate_criterion,
|
||||||
|
error_code_mapping: block.error_code_mapping,
|
||||||
|
parameter_keys: block.parameters.map((p) => p.key),
|
||||||
|
};
|
||||||
|
return blockYaml;
|
||||||
|
}
|
||||||
case "for_loop": {
|
case "for_loop": {
|
||||||
const blockYaml: ForLoopBlockYAML = {
|
const blockYaml: ForLoopBlockYAML = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -189,6 +189,14 @@ export type FileURLParserBlock = WorkflowBlockBase & {
|
|||||||
file_type: "csv";
|
file_type: "csv";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValidationBlock = WorkflowBlockBase & {
|
||||||
|
block_type: "validation";
|
||||||
|
complete_criterion: string | null;
|
||||||
|
terminate_criterion: string | null;
|
||||||
|
error_code_mapping: Record<string, string> | null;
|
||||||
|
parameters: Array<WorkflowParameter>;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowBlock =
|
export type WorkflowBlock =
|
||||||
| TaskBlock
|
| TaskBlock
|
||||||
| ForLoopBlock
|
| ForLoopBlock
|
||||||
@@ -197,7 +205,8 @@ export type WorkflowBlock =
|
|||||||
| UploadToS3Block
|
| UploadToS3Block
|
||||||
| DownloadToS3Block
|
| DownloadToS3Block
|
||||||
| SendEmailBlock
|
| SendEmailBlock
|
||||||
| FileURLParserBlock;
|
| FileURLParserBlock
|
||||||
|
| ValidationBlock;
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<Parameter>;
|
parameters: Array<Parameter>;
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const BlockTypes = {
|
|||||||
UPLOAD_TO_S3: "upload_to_s3",
|
UPLOAD_TO_S3: "upload_to_s3",
|
||||||
SEND_EMAIL: "send_email",
|
SEND_EMAIL: "send_email",
|
||||||
FILE_URL_PARSER: "file_url_parser",
|
FILE_URL_PARSER: "file_url_parser",
|
||||||
|
VALIDATION: "validation",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||||
@@ -87,7 +88,8 @@ export type BlockYAML =
|
|||||||
| UploadToS3BlockYAML
|
| UploadToS3BlockYAML
|
||||||
| SendEmailBlockYAML
|
| SendEmailBlockYAML
|
||||||
| FileUrlParserBlockYAML
|
| FileUrlParserBlockYAML
|
||||||
| ForLoopBlockYAML;
|
| ForLoopBlockYAML
|
||||||
|
| ValidationBlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: BlockType;
|
block_type: BlockType;
|
||||||
@@ -113,6 +115,14 @@ export type TaskBlockYAML = BlockYAMLBase & {
|
|||||||
cache_actions: boolean;
|
cache_actions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValidationBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "validation";
|
||||||
|
complete_criterion: string | null;
|
||||||
|
terminate_criterion: string | null;
|
||||||
|
error_code_mapping: Record<string, string> | null;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type CodeBlockYAML = BlockYAMLBase & {
|
export type CodeBlockYAML = BlockYAMLBase & {
|
||||||
block_type: "code";
|
block_type: "code";
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user