Add workflows settings in start node (#1270)
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
AWSSecretParameter,
|
||||
WorkflowApiResponse,
|
||||
WorkflowParameterValueType,
|
||||
WorkflowSettings,
|
||||
} from "../types/workflowTypes";
|
||||
import {
|
||||
BitwardenLoginCredentialParameterYAML,
|
||||
@@ -50,7 +51,12 @@ import {
|
||||
import { WorkflowHeader } from "./WorkflowHeader";
|
||||
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||
import { edgeTypes } from "./edges";
|
||||
import { AppNode, nodeTypes, WorkflowBlockNode } from "./nodes";
|
||||
import {
|
||||
AppNode,
|
||||
isWorkflowBlockNode,
|
||||
nodeTypes,
|
||||
WorkflowBlockNode,
|
||||
} from "./nodes";
|
||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||
import "./reactFlowOverrideStyles.css";
|
||||
@@ -63,6 +69,7 @@ import {
|
||||
getOutputParameterKey,
|
||||
getWorkflowBlocks,
|
||||
getWorkflowErrors,
|
||||
getWorkflowSettings,
|
||||
layout,
|
||||
nodeAdderNode,
|
||||
startNode,
|
||||
@@ -200,6 +207,7 @@ function FlowRenderer({
|
||||
parameters: Array<ParameterYAML>;
|
||||
blocks: Array<BlockYAML>;
|
||||
title: string;
|
||||
settings: WorkflowSettings;
|
||||
}) => {
|
||||
if (!workflowPermanentId) {
|
||||
return;
|
||||
@@ -208,8 +216,9 @@ function FlowRenderer({
|
||||
const requestBody: WorkflowCreateYAMLRequest = {
|
||||
title: data.title,
|
||||
description: workflow.description,
|
||||
proxy_location: workflow.proxy_location,
|
||||
webhook_callback_url: workflow.webhook_callback_url,
|
||||
proxy_location: data.settings.proxyLocation,
|
||||
webhook_callback_url: data.settings.webhookCallbackUrl,
|
||||
persist_browser_session: data.settings.persistBrowserSession,
|
||||
totp_verification_url: workflow.totp_verification_url,
|
||||
workflow_definition: {
|
||||
parameters: data.parameters,
|
||||
@@ -267,6 +276,7 @@ function FlowRenderer({
|
||||
|
||||
async function handleSave() {
|
||||
const blocks = getWorkflowBlocks(nodes, edges);
|
||||
const settings = getWorkflowSettings(nodes);
|
||||
const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters);
|
||||
const filteredParameters = workflow.workflow_definition.parameters.filter(
|
||||
(parameter) => {
|
||||
@@ -295,6 +305,7 @@ function FlowRenderer({
|
||||
],
|
||||
blocks,
|
||||
title,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,10 +319,13 @@ function FlowRenderer({
|
||||
const newNodes: Array<AppNode> = [];
|
||||
const newEdges: Array<Edge> = [];
|
||||
const id = nanoid();
|
||||
const existingLabels = nodes
|
||||
.filter(isWorkflowBlockNode)
|
||||
.map((node) => node.data.label);
|
||||
const node = createNode(
|
||||
{ id, parentId: parent },
|
||||
nodeType,
|
||||
generateNodeLabel(nodes.map((node) => node.data.label)),
|
||||
generateNodeLabel(existingLabels),
|
||||
);
|
||||
newNodes.push(node);
|
||||
if (previous) {
|
||||
@@ -343,7 +357,9 @@ function FlowRenderer({
|
||||
// when loop node is first created it needs an adder node so nodes can be added inside the loop
|
||||
const startNodeId = nanoid();
|
||||
const adderNodeId = nanoid();
|
||||
newNodes.push(startNode(startNodeId, id));
|
||||
newNodes.push(
|
||||
startNode(startNodeId, { withWorkflowSettings: false }, id),
|
||||
);
|
||||
newNodes.push(nodeAdderNode(adderNodeId, id));
|
||||
newEdges.push(defaultEdge(startNodeId, adderNodeId));
|
||||
}
|
||||
@@ -370,7 +386,7 @@ function FlowRenderer({
|
||||
|
||||
function deleteNode(id: string) {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node) {
|
||||
if (!node || !isWorkflowBlockNode(node)) {
|
||||
return;
|
||||
}
|
||||
const deletedNodeLabel = node.data.label;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import { FlowRenderer } from "./FlowRenderer";
|
||||
import { getElements } from "./workflowEditorUtils";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
import { WorkflowSettings } from "../types/workflowTypes";
|
||||
|
||||
function WorkflowEditor() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
@@ -39,7 +40,13 @@ function WorkflowEditor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elements = getElements(workflow.workflow_definition.blocks);
|
||||
const settings: WorkflowSettings = {
|
||||
persistBrowserSession: workflow.persist_browser_session,
|
||||
proxyLocation: workflow.proxy_location,
|
||||
webhookCallbackUrl: workflow.webhook_callback_url,
|
||||
};
|
||||
|
||||
const elements = getElements(workflow.workflow_definition.blocks, settings);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
|
||||
@@ -1,6 +1,109 @@
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import type { StartNode } from "./types";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { useState } from "react";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ProxySelector } from "@/components/ProxySelector";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [inputs, setInputs] = useState({
|
||||
webhookCallbackUrl: data.withWorkflowSettings
|
||||
? data.webhookCallbackUrl
|
||||
: "",
|
||||
proxyLocation: data.withWorkflowSettings
|
||||
? data.proxyLocation
|
||||
: ProxyLocation.Residential,
|
||||
persistBrowserSession: data.withWorkflowSettings
|
||||
? data.persistBrowserSession
|
||||
: false,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
setInputs({ ...inputs, [key]: value });
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
if (data.withWorkflowSettings) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center">
|
||||
<div className="space-y-4">
|
||||
<header>Start</header>
|
||||
<Separator />
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="settings" className="border-b-0">
|
||||
<AccordionTrigger className="py-2">
|
||||
Workflow Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label>Webhook Callback URL</Label>
|
||||
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
|
||||
</div>
|
||||
<Input
|
||||
value={inputs.webhookCallbackUrl}
|
||||
placeholder="https://"
|
||||
onChange={(event) => {
|
||||
handleChange(
|
||||
"webhookCallbackUrl",
|
||||
event.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label>Proxy Location</Label>
|
||||
<HelpTooltip content="Route Skyvern through one of our available proxies." />
|
||||
</div>
|
||||
<ProxySelector
|
||||
value={inputs.proxyLocation}
|
||||
onChange={(value) => {
|
||||
handleChange("proxyLocation", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Persist Browser Session</Label>
|
||||
<HelpTooltip content="Persist session information across workflow runs" />
|
||||
<Switch
|
||||
checked={inputs.persistBrowserSession}
|
||||
onCheckedChange={(value) => {
|
||||
handleChange("persistBrowserSession", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StartNode() {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { AppNode } from "..";
|
||||
|
||||
export type StartNodeData = Record<string, never>;
|
||||
export type WorkflowStartNodeData = {
|
||||
withWorkflowSettings: true;
|
||||
webhookCallbackUrl: string;
|
||||
proxyLocation: ProxyLocation;
|
||||
persistBrowserSession: boolean;
|
||||
};
|
||||
|
||||
export type OtherStartNodeData = {
|
||||
withWorkflowSettings: false;
|
||||
};
|
||||
|
||||
export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData;
|
||||
|
||||
export type StartNode = Node<StartNodeData, "start">;
|
||||
|
||||
export function isStartNode(node: AppNode): node is StartNode {
|
||||
return node.type === "start";
|
||||
}
|
||||
|
||||
export function isWorkflowStartNodeData(
|
||||
data: StartNodeData,
|
||||
): data is WorkflowStartNodeData {
|
||||
return data.withWorkflowSettings;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
WorkflowApiResponse,
|
||||
WorkflowBlock,
|
||||
WorkflowParameterValueType,
|
||||
WorkflowSettings,
|
||||
} from "../types/workflowTypes";
|
||||
import {
|
||||
ActionBlockYAML,
|
||||
@@ -53,7 +54,12 @@ import {
|
||||
} from "./nodes/LoopNode/types";
|
||||
import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
|
||||
import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
|
||||
import { StartNode } from "./nodes/StartNode/types";
|
||||
import {
|
||||
isStartNode,
|
||||
isWorkflowStartNodeData,
|
||||
StartNode,
|
||||
StartNodeData,
|
||||
} from "./nodes/StartNode/types";
|
||||
import { isTaskNode, taskNodeDefaultData } from "./nodes/TaskNode/types";
|
||||
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
||||
import { NodeBaseData } from "./nodes/types";
|
||||
@@ -73,6 +79,7 @@ import {
|
||||
} from "./nodes/ExtractionNode/types";
|
||||
import { loginNodeDefaultData } from "./nodes/LoginNode/types";
|
||||
import { waitNodeDefaultData } from "./nodes/WaitNode/types";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
@@ -458,12 +465,16 @@ export function edgeWithAddButton(source: string, target: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function startNode(id: string, parentId?: string): StartNode {
|
||||
export function startNode(
|
||||
id: string,
|
||||
data: StartNodeData,
|
||||
parentId?: string,
|
||||
): StartNode {
|
||||
const node: StartNode = {
|
||||
id,
|
||||
type: "start",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
data,
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
};
|
||||
@@ -488,7 +499,10 @@ export function nodeAdderNode(id: string, parentId?: string): NodeAdderNode {
|
||||
return node;
|
||||
}
|
||||
|
||||
function getElements(blocks: Array<WorkflowBlock>): {
|
||||
function getElements(
|
||||
blocks: Array<WorkflowBlock>,
|
||||
settings: WorkflowSettings,
|
||||
): {
|
||||
nodes: Array<AppNode>;
|
||||
edges: Array<Edge>;
|
||||
} {
|
||||
@@ -497,7 +511,14 @@ function getElements(blocks: Array<WorkflowBlock>): {
|
||||
const edges: Array<Edge> = [];
|
||||
|
||||
const startNodeId = nanoid();
|
||||
nodes.push(startNode(startNodeId));
|
||||
nodes.push(
|
||||
startNode(startNodeId, {
|
||||
withWorkflowSettings: true,
|
||||
persistBrowserSession: settings.persistBrowserSession,
|
||||
proxyLocation: settings.proxyLocation ?? ProxyLocation.Residential,
|
||||
webhookCallbackUrl: settings.webhookCallbackUrl ?? "",
|
||||
}),
|
||||
);
|
||||
|
||||
data.forEach((d, index) => {
|
||||
const node = convertToNode(
|
||||
@@ -519,7 +540,15 @@ function getElements(blocks: Array<WorkflowBlock>): {
|
||||
const loopBlocks = data.filter((d) => d.block.block_type === "for_loop");
|
||||
loopBlocks.forEach((block) => {
|
||||
const startNodeId = nanoid();
|
||||
nodes.push(startNode(startNodeId, block.id));
|
||||
nodes.push(
|
||||
startNode(
|
||||
startNodeId,
|
||||
{
|
||||
withWorkflowSettings: false,
|
||||
},
|
||||
block.id,
|
||||
),
|
||||
);
|
||||
const children = data.filter((b) => b.parentId === block.id);
|
||||
if (children.length === 0) {
|
||||
const adderNodeId = nanoid();
|
||||
@@ -555,7 +584,7 @@ function createNode(
|
||||
identifiers: { id: string; parentId?: string },
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>,
|
||||
label: string,
|
||||
): AppNode {
|
||||
): WorkflowBlockNode {
|
||||
const common = {
|
||||
draggable: false,
|
||||
position: { x: 0, y: 0 },
|
||||
@@ -991,6 +1020,30 @@ function getWorkflowBlocks(
|
||||
return getWorkflowBlocksUtil(nodes, edges);
|
||||
}
|
||||
|
||||
function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
|
||||
const defaultSettings = {
|
||||
persistBrowserSession: false,
|
||||
proxyLocation: ProxyLocation.Residential,
|
||||
webhookCallbackUrl: null,
|
||||
};
|
||||
const startNodes = nodes.filter(isStartNode);
|
||||
const startNodeWithWorkflowSettings = startNodes.find(
|
||||
(node) => node.data.withWorkflowSettings,
|
||||
);
|
||||
if (!startNodeWithWorkflowSettings) {
|
||||
return defaultSettings;
|
||||
}
|
||||
const data = startNodeWithWorkflowSettings.data;
|
||||
if (isWorkflowStartNodeData(data)) {
|
||||
return {
|
||||
persistBrowserSession: data.persistBrowserSession,
|
||||
proxyLocation: data.proxyLocation,
|
||||
webhookCallbackUrl: data.webhookCallbackUrl,
|
||||
};
|
||||
}
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
function generateNodeLabel(existingLabels: Array<string>) {
|
||||
for (let i = 1; i < existingLabels.length + 2; i++) {
|
||||
const label = NEW_NODE_LABEL_PREFIX + i;
|
||||
@@ -1608,6 +1661,7 @@ export {
|
||||
getBlockNameOfOutputParameterKey,
|
||||
getDefaultValueForParameterType,
|
||||
getElements,
|
||||
getWorkflowSettings,
|
||||
getOutputParameterKey,
|
||||
getPreviousNodeIds,
|
||||
getUniqueLabelForExistingNode,
|
||||
|
||||
Reference in New Issue
Block a user