From e0aadac962f5cd65880a6019d18b0b667ea205a6 Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Tue, 26 Nov 2024 10:41:18 -0800 Subject: [PATCH] Add workflows settings in start node (#1270) --- .../src/components/ProxySelector.tsx | 5 +- .../routes/tasks/create/CreateNewTaskForm.tsx | 1 + .../src/routes/tasks/create/SavedTaskForm.tsx | 1 + .../src/routes/workflows/RunWorkflowForm.tsx | 45 +++++--- .../workflows/WorkflowRunParameters.tsx | 5 + .../routes/workflows/editor/FlowRenderer.tsx | 28 ++++- .../workflows/editor/WorkflowEditor.tsx | 9 +- .../editor/nodes/StartNode/StartNode.tsx | 107 +++++++++++++++++- .../workflows/editor/nodes/StartNode/types.ts | 25 +++- .../workflows/editor/workflowEditorUtils.ts | 68 +++++++++-- .../workflows/hooks/useLabelChangeHandler.ts | 6 +- .../routes/workflows/types/workflowTypes.ts | 16 ++- .../workflows/types/workflowYamlTypes.ts | 1 + 13 files changed, 275 insertions(+), 42 deletions(-) diff --git a/skyvern-frontend/src/components/ProxySelector.tsx b/skyvern-frontend/src/components/ProxySelector.tsx index 91061bf7..a052e2a5 100644 --- a/skyvern-frontend/src/components/ProxySelector.tsx +++ b/skyvern-frontend/src/components/ProxySelector.tsx @@ -10,12 +10,13 @@ import { type Props = { value: ProxyLocation | null; onChange: (value: ProxyLocation) => void; + className?: string; }; -function ProxySelector({ value, onChange }: Props) { +function ProxySelector({ value, onChange, className }: Props) { return ( { + handleChange( + "webhookCallbackUrl", + event.target.value, + ); + }} + /> + +
+
+ + +
+ { + handleChange("proxyLocation", value); + }} + /> +
+
+
+ + + { + handleChange("persistBrowserSession", value); + }} + /> +
+
+ + + + + + + + ); + } -function StartNode() { return (
; +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; + +export function isStartNode(node: AppNode): node is StartNode { + return node.type === "start"; +} + +export function isWorkflowStartNodeData( + data: StartNodeData, +): data is WorkflowStartNodeData { + return data.withWorkflowSettings; +} diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index eddde9d3..f53cfa72 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -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): { +function getElements( + blocks: Array, + settings: WorkflowSettings, +): { nodes: Array; edges: Array; } { @@ -497,7 +511,14 @@ function getElements(blocks: Array): { const edges: Array = []; 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): { 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, 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): 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) { 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, diff --git a/skyvern-frontend/src/routes/workflows/hooks/useLabelChangeHandler.ts b/skyvern-frontend/src/routes/workflows/hooks/useLabelChangeHandler.ts index ae8660e8..d89252c4 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useLabelChangeHandler.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useLabelChangeHandler.ts @@ -1,5 +1,5 @@ import { useNodes, useReactFlow } from "@xyflow/react"; -import { AppNode } from "../editor/nodes"; +import { AppNode, isWorkflowBlockNode } from "../editor/nodes"; import { getUniqueLabelForExistingNode, getUpdatedNodesAfterLabelUpdateForParameterKeys, @@ -21,7 +21,9 @@ function useNodeLabelChangeHandler({ id, initialValue }: Props) { useWorkflowParametersState(); function handleLabelChange(value: string) { - const existingLabels = nodes.map((n) => n.data.label); + const existingLabels = nodes + .filter(isWorkflowBlockNode) + .map((n) => n.data.label); const labelWithoutWhitespace = value.replace(/\s+/g, "_"); const newLabel = getUniqueLabelForExistingNode( labelWithoutWhitespace, diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 443dd32d..caa87303 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -1,3 +1,5 @@ +import { ProxyLocation } from "@/api/types"; + export type WorkflowParameterBase = { parameter_type: WorkflowParameterType; key: string; @@ -296,14 +298,22 @@ export type WorkflowApiResponse = { version: number; description: string; workflow_definition: WorkflowDefinition; - proxy_location: string; - webhook_callback_url: string; - totp_verification_url: string; + proxy_location: ProxyLocation | null; + webhook_callback_url: string | null; + persist_browser_session: boolean; + totp_verification_url: string | null; + totp_identifier: string | null; created_at: string; modified_at: string; deleted_at: string | null; }; +export type WorkflowSettings = { + proxyLocation: ProxyLocation | null; + webhookCallbackUrl: string | null; + persistBrowserSession: boolean; +}; + export function isOutputParameter( parameter: Parameter, ): parameter is OutputParameter { diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index c0881dfd..648ff8ca 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -3,6 +3,7 @@ export type WorkflowCreateYAMLRequest = { description?: string | null; proxy_location?: string | null; webhook_callback_url?: string | null; + persist_browser_session?: boolean; totp_verification_url?: string | null; workflow_definition: WorkflowDefinitionYAML; is_saved_task?: boolean;