diff --git a/skyvern-frontend/package-lock.json b/skyvern-frontend/package-lock.json index d7b8da26..c961e99c 100644 --- a/skyvern-frontend/package-lock.json +++ b/skyvern-frontend/package-lock.json @@ -48,8 +48,10 @@ "nanoid": "^5.0.7", "open": "^10.1.0", "posthog-js": "^1.138.0", + "re-resizable": "^6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.5.0", "react-github-btn": "^1.4.0", "react-hook-form": "^7.51.1", "react-router-dom": "^6.22.3", @@ -4692,9 +4694,10 @@ } }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -7372,6 +7375,17 @@ } } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7453,6 +7467,16 @@ "node": ">= 0.8" } }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -7476,6 +7500,20 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-github-btn": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz", @@ -7502,6 +7540,12 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", diff --git a/skyvern-frontend/package.json b/skyvern-frontend/package.json index ee81961b..34dbe4e3 100644 --- a/skyvern-frontend/package.json +++ b/skyvern-frontend/package.json @@ -56,8 +56,10 @@ "nanoid": "^5.0.7", "open": "^10.1.0", "posthog-js": "^1.138.0", + "re-resizable": "^6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.5.0", "react-github-btn": "^1.4.0", "react-hook-form": "^7.51.1", "react-router-dom": "^6.22.3", diff --git a/skyvern-frontend/src/components/BrowserStream.tsx b/skyvern-frontend/src/components/BrowserStream.tsx index 25af7d53..a62dafdd 100644 --- a/skyvern-frontend/src/components/BrowserStream.tsx +++ b/skyvern-frontend/src/components/BrowserStream.tsx @@ -1,6 +1,6 @@ import { Status } from "@/api/types"; import { useEffect, useState, useRef, useCallback } from "react"; -import { HandIcon, PlayIcon } from "@radix-ui/react-icons"; +import { HandIcon, StopIcon } from "@radix-ui/react-icons"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { statusIsNotFinalized } from "@/routes/tasks/types"; @@ -317,13 +317,15 @@ function BrowserStream({
diff --git a/skyvern-frontend/src/components/Timer.tsx b/skyvern-frontend/src/components/Timer.tsx new file mode 100644 index 00000000..415eb0fc --- /dev/null +++ b/skyvern-frontend/src/components/Timer.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; + +interface HMS { + hour: number; + minute: number; + second: number; +} + +interface Props { + startAt?: HMS; +} + +function Timer({ startAt }: Props) { + const [time, setTime] = useState({ + hour: 0, + minute: 0, + second: 0, + }); + + useEffect(() => { + const start = performance.now(); + + const loop = () => { + const elapsed = performance.now() - start; + let seconds = Math.floor(elapsed / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + seconds = seconds % 60; + minutes = minutes % 60; + hours = hours % 24; + setTime(() => ({ + hour: hours + (startAt?.hour ?? 0), + minute: minutes + (startAt?.minute ?? 0), + second: seconds + (startAt?.second ?? 0), + })); + + rAF = requestAnimationFrame(loop); + }; + + let rAF = requestAnimationFrame(loop); + + return () => cancelAnimationFrame(rAF); + }, [startAt]); + + return ( +
+ {String(time.hour).padStart(2, "0")}: + {String(time.minute).padStart(2, "0")}: + {String(time.second).padStart(2, "0")} +
+ ); +} + +export { Timer }; diff --git a/skyvern-frontend/src/hooks/useOnChange.ts b/skyvern-frontend/src/hooks/useOnChange.ts new file mode 100644 index 00000000..592df4d6 --- /dev/null +++ b/skyvern-frontend/src/hooks/useOnChange.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from "react"; + +function useOnChange( + value: T, + callback: (newValue: T, prevValue: T | undefined) => void, +) { + const prevValue = useRef(value); + + useEffect(() => { + if (prevValue.current !== undefined) { + callback(value, prevValue.current); + } + prevValue.current = value; + }, [value, callback]); +} + +export { useOnChange }; diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index 91bc5d0c..0b5725dc 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -22,11 +22,16 @@ import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/Workfl import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput"; import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview"; import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording"; +import { DebugStoreProvider } from "@/store/DebugStoreContext"; const router = createBrowserRouter([ { path: "/", - element: , + element: ( + + + + ), children: [ { index: true, @@ -98,6 +103,14 @@ const router = createBrowserRouter([ index: true, element: , }, + { + path: "debug", + element: , + }, + { + path: ":workflowRunId/:blockLabel/debug", + element: , + }, { path: "edit", element: , diff --git a/skyvern-frontend/src/routes/root/Header.tsx b/skyvern-frontend/src/routes/root/Header.tsx index ef7d1cb5..e018f4c8 100644 --- a/skyvern-frontend/src/routes/root/Header.tsx +++ b/skyvern-frontend/src/routes/root/Header.tsx @@ -1,10 +1,15 @@ import { DiscordLogoIcon } from "@radix-ui/react-icons"; import GitHubButton from "react-github-btn"; -import { Link, useMatch } from "react-router-dom"; +import { Link, useMatch, useSearchParams } from "react-router-dom"; import { NavigationHamburgerMenu } from "./NavigationHamburgerMenu"; function Header() { - const match = useMatch("/workflows/:workflowPermanentId/edit"); + const [searchParams] = useSearchParams(); + const embed = searchParams.get("embed"); + const match = + useMatch("/workflows/:workflowPermanentId/edit") || + location.pathname.includes("debug") || + embed === "true"; if (match) { return null; diff --git a/skyvern-frontend/src/routes/root/RootLayout.tsx b/skyvern-frontend/src/routes/root/RootLayout.tsx index 9235f769..0fcee1a2 100644 --- a/skyvern-frontend/src/routes/root/RootLayout.tsx +++ b/skyvern-frontend/src/routes/root/RootLayout.tsx @@ -4,18 +4,24 @@ import { cn } from "@/util/utils"; import { Outlet } from "react-router-dom"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; +import { useDebugStore } from "@/store/useDebugStore"; function RootLayout() { const collapsed = useSidebarStore((state) => state.collapsed); + const embed = new URLSearchParams(window.location.search).get("embed"); + const isEmbedded = embed === "true"; + const debugStore = useDebugStore(); return ( <> + {!isEmbedded && }
-
diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 11f1c50c..9391d1cb 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -30,6 +30,8 @@ import { WorkflowParameterInput } from "./WorkflowParameterInput"; import { AxiosError } from "axios"; import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils"; import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types"; +import { lsKeys } from "@/util/env"; + type Props = { workflowParameters: Array; initialValues: Record; @@ -145,7 +147,7 @@ function RunWorkflowForm({ const navigate = useNavigate(); const queryClient = useQueryClient(); const browserSessionIdDefault = useLocalStorageFormDefault( - "skyvern.browserSessionId", + lsKeys.browserSessionId, (initialValues.browserSessionId as string | undefined) ?? null, ); const form = useForm({ @@ -162,11 +164,7 @@ function RunWorkflowForm({ }); const apiCredential = useApiCredential(); - useSyncFormFieldToStorage( - form, - "browserSessionId", - "skyvern.browserSessionId", - ); + useSyncFormFieldToStorage(form, "browserSessionId", lsKeys.browserSessionId); const runWorkflowMutation = useMutation({ mutationFn: async (values: RunWorkflowFormType) => { diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx index a9013894..c93772c5 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx @@ -41,6 +41,8 @@ import { type ApiCommandOptions } from "@/util/apiCommands"; function WorkflowRun() { const [searchParams, setSearchParams] = useSearchParams(); + const embed = searchParams.get("embed"); + const isEmbedded = embed === "true"; const active = searchParams.get("active"); const { workflowRunId, workflowPermanentId } = useParams(); const credentialGetter = useCredentialGetter(); @@ -169,92 +171,94 @@ function WorkflowRun() { return (
-
-
-
- {title} - {workflowRunIsLoading ? ( - - ) : workflowRun ? ( - - ) : null} + {!isEmbedded && ( +
+
+
+ {title} + {workflowRunIsLoading ? ( + + ) : workflowRun ? ( + + ) : null} +
+

{workflowRunId}

-

{workflowRunId}

-
-
- - ({ - method: "POST", - url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`, - body: { - data: workflowRun?.parameters, - proxy_location: "RESIDENTIAL", - }, - headers: { - "Content-Type": "application/json", - "x-api-key": apiCredential ?? "", - }, - }) satisfies ApiCommandOptions - } - /> - - {workflowRunIsRunningOrQueued && ( - - - - - - - Are you sure? - - Are you sure you want to cancel this workflow run? - - - - - - - - - - - )} - {workflowRunIsFinalized && !isTaskv2Run && ( - - )} -
-
+ {workflowRunIsRunningOrQueued && ( + + + + + + + Are you sure? + + Are you sure you want to cancel this workflow run? + + + + + + + + + + + )} + {workflowRunIsFinalized && !isTaskv2Run && ( + + )} +
+
+ )} {showOutputSection && (
)} {workflowFailureReason} - + {!isEmbedded && ( + + )}
diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRunParameters.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRunParameters.tsx index ad3a5160..ebcc99a0 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRunParameters.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRunParameters.tsx @@ -6,6 +6,7 @@ import { RunWorkflowForm } from "./RunWorkflowForm"; import { WorkflowApiResponse } from "./types/workflowTypes"; import { Skeleton } from "@/components/ui/skeleton"; import { ProxyLocation } from "@/api/types"; +import { getInitialValues } from "./utils"; function WorkflowRunParameters() { const credentialGetter = useCredentialGetter(); @@ -30,6 +31,7 @@ function WorkflowRunParameters() { const proxyLocation = location.state ? (location.state.proxyLocation as ProxyLocation) : null; + const maxScreenshotScrolls = location.state?.maxScreenshotScrolls ?? null; const webhookCallbackUrl = location.state @@ -40,43 +42,7 @@ function WorkflowRunParameters() { ? (location.state.extraHttpHeaders as Record) : null; - const initialValues = location.state?.data - ? location.state.data - : workflowParameters?.reduce( - (acc, curr) => { - if (curr.workflow_parameter_type === "json") { - if (typeof curr.default_value === "string") { - acc[curr.key] = curr.default_value; - return acc; - } - if (curr.default_value) { - acc[curr.key] = JSON.stringify(curr.default_value, null, 2); - return acc; - } - } - if ( - curr.default_value && - curr.workflow_parameter_type === "boolean" - ) { - acc[curr.key] = Boolean(curr.default_value); - return acc; - } - if ( - curr.default_value === null && - curr.workflow_parameter_type === "string" - ) { - acc[curr.key] = ""; - return acc; - } - if (curr.default_value) { - acc[curr.key] = curr.default_value; - return acc; - } - acc[curr.key] = null; - return acc; - }, - {} as Record, - ); + const initialValues = getInitialValues(location, workflowParameters ?? []); const header = (
diff --git a/skyvern-frontend/src/routes/workflows/WorkflowsPageLayout.tsx b/skyvern-frontend/src/routes/workflows/WorkflowsPageLayout.tsx index 2331422b..66ac292b 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowsPageLayout.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowsPageLayout.tsx @@ -1,9 +1,13 @@ import { cn } from "@/util/utils"; -import { Outlet, useMatch } from "react-router-dom"; +import { Outlet, useMatch, useSearchParams } from "react-router-dom"; function WorkflowsPageLayout() { - const match = useMatch("/workflows/:workflowPermanentId/edit"); - + const [searchParams] = useSearchParams(); + const embed = searchParams.get("embed"); + const match = + useMatch("/workflows/:workflowPermanentId/edit") || + location.pathname.includes("debug") || + embed === "true"; return (
(initialParameters); const [title, setTitle] = useState(initialTitle); + const [debuggableBlockCount, setDebuggableBlockCount] = useState(0); const nodesInitialized = useNodesInitialized(); const { hasChanges, setHasChanges } = useWorkflowHasChangesStore(); useShouldNotifyWhenClosingTab(hasChanges); @@ -379,6 +387,14 @@ function FlowRenderer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodesInitialized]); + useEffect(() => { + const blocks = getWorkflowBlocks(nodes, edges); + const debuggable = blocks.filter((block) => + debuggableWorkflowBlockTypes.has(block.block_type), + ); + setDebuggableBlockCount(debuggable.length); + }, [nodes, edges]); + async function handleSave() { const blocks = getWorkflowBlocks(nodes, edges); const settings = getWorkflowSettings(nodes); @@ -590,6 +606,47 @@ function FlowRenderer({ useAutoPan(editorElementRef, nodes); + const zoomLock = 1 as const; + const yLockMax = 140 as const; + + /** + * TODO(jdo): hack + */ + const getXLock = () => { + const hasForLoopNode = nodes.some((node) => node.type === "loop"); + return hasForLoopNode ? 24 : 104; + }; + + useOnChange(debugStore.isDebugMode, (newValue) => { + const xLock = getXLock(); + if (newValue) { + const currentY = reactFlowInstance.getViewport().y; + reactFlowInstance.setViewport({ x: xLock, y: currentY, zoom: zoomLock }); + } + }); + + const constrainPan = (y: number) => { + const yLockMin = nodes.reduce( + (acc, node) => { + const nodeBottom = node.position.y + (node.height ?? 0); + if (nodeBottom > acc.value) { + return { value: nodeBottom }; + } + return acc; + }, + { value: -Infinity }, + ); + + const yLockMinValue = yLockMin.value; + const xLock = getXLock(); + const newY = Math.max(-yLockMinValue + yLockMax, Math.min(yLockMax, y)); + reactFlowInstance.setViewport({ + x: xLock, + y: newY, + zoom: zoomLock, + }); + }; + return ( <> { + const y = viewport.y; + debugStore.isDebugMode && constrainPan(y); + }} + maxZoom={debugStore.isDebugMode ? 1 : 2} + minZoom={debugStore.isDebugMode ? 1 : 0.5} + panOnDrag={!debugStore.isDebugMode} + panOnScroll={debugStore.isDebugMode} + panOnScrollMode={ + debugStore.isDebugMode + ? PanOnScrollMode.Vertical + : PanOnScrollMode.Free + } + zoomOnDoubleClick={!debugStore.isDebugMode} + zoomOnPinch={!debugStore.isDebugMode} + zoomOnScroll={!debugStore.isDebugMode} > - + { diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx index f8b49660..02378bf4 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx @@ -7,6 +7,7 @@ import { useWorkflowQuery } from "../hooks/useWorkflowQuery"; import { FlowRenderer } from "./FlowRenderer"; import { getElements } from "./workflowEditorUtils"; import { LogoMinimized } from "@/components/LogoMinimized"; +import { useDebugStore } from "@/store/useDebugStore"; import { isDisplayedInWorkflowEditor, WorkflowEditorParameterTypes, @@ -16,8 +17,11 @@ import { } from "../types/workflowTypes"; import { ParametersState } from "./types"; import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery"; +import { WorkflowDebugOverviewWindow } from "./panels/WorkflowDebugOverviewWindow"; +import { cn } from "@/util/utils"; function WorkflowEditor() { + const debugStore = useDebugStore(); const { workflowPermanentId } = useParams(); const setCollapsed = useSidebarStore((state) => { return state.setCollapsed; @@ -72,110 +76,123 @@ function WorkflowEditor() { !isGlobalWorkflow, ); - return ( -
- - isDisplayedInWorkflowEditor(parameter)) - .map((parameter) => { - if ( - parameter.parameter_type === WorkflowParameterTypes.Workflow - ) { - if ( - parameter.workflow_parameter_type === - WorkflowParameterValueType.CredentialId - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Credential, - credentialId: parameter.default_value as string, - description: parameter.description, - }; - } - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Workflow, - dataType: parameter.workflow_parameter_type, - defaultValue: parameter.default_value, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === WorkflowParameterTypes.Context - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Context, - sourceParameterKey: parameter.source.key, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === - WorkflowParameterTypes.Bitwarden_Sensitive_Information - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Secret, - collectionId: parameter.bitwarden_collection_id, - identityKey: parameter.bitwarden_identity_key, - identityFields: parameter.bitwarden_identity_fields, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === - WorkflowParameterTypes.Bitwarden_Credit_Card_Data - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.CreditCardData, - collectionId: parameter.bitwarden_collection_id, - itemId: parameter.bitwarden_item_id, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === WorkflowParameterTypes.Credential - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Credential, - credentialId: parameter.credential_id, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === - WorkflowParameterTypes.OnePassword - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.OnePassword, - vaultId: parameter.vault_id, - itemId: parameter.item_id, - description: parameter.description, - }; - } else if ( - parameter.parameter_type === - WorkflowParameterTypes.Bitwarden_Login_Credential - ) { - return { - key: parameter.key, - parameterType: WorkflowEditorParameterTypes.Credential, - collectionId: parameter.bitwarden_collection_id, - itemId: parameter.bitwarden_item_id, - urlParameterKey: parameter.url_parameter_key, - description: parameter.description, - }; - } - return undefined; - }) - .filter(Boolean) as ParametersState + const getInitialParameters = () => { + return workflow.workflow_definition.parameters + .filter((parameter) => isDisplayedInWorkflowEditor(parameter)) + .map((parameter) => { + if (parameter.parameter_type === WorkflowParameterTypes.Workflow) { + if ( + parameter.workflow_parameter_type === + WorkflowParameterValueType.CredentialId + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Credential, + credentialId: parameter.default_value as string, + description: parameter.description, + }; } - workflow={workflow} - /> - + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Workflow, + dataType: parameter.workflow_parameter_type, + defaultValue: parameter.default_value, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === WorkflowParameterTypes.Context + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Context, + sourceParameterKey: parameter.source.key, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === + WorkflowParameterTypes.Bitwarden_Sensitive_Information + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Secret, + collectionId: parameter.bitwarden_collection_id, + identityKey: parameter.bitwarden_identity_key, + identityFields: parameter.bitwarden_identity_fields, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === + WorkflowParameterTypes.Bitwarden_Credit_Card_Data + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.CreditCardData, + collectionId: parameter.bitwarden_collection_id, + itemId: parameter.bitwarden_item_id, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === WorkflowParameterTypes.Credential + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Credential, + credentialId: parameter.credential_id, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === WorkflowParameterTypes.OnePassword + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.OnePassword, + vaultId: parameter.vault_id, + itemId: parameter.item_id, + description: parameter.description, + }; + } else if ( + parameter.parameter_type === + WorkflowParameterTypes.Bitwarden_Login_Credential + ) { + return { + key: parameter.key, + parameterType: WorkflowEditorParameterTypes.Credential, + collectionId: parameter.bitwarden_collection_id, + itemId: parameter.bitwarden_item_id, + urlParameterKey: parameter.url_parameter_key, + description: parameter.description, + }; + } + return undefined; + }) + .filter(Boolean) as ParametersState; + }; + + return ( +
+
+ + + +
+ {debugStore.isDebugMode && ( +
+ +
+ )}
); } diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx index 537c3445..64e0eeeb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx @@ -10,6 +10,7 @@ import { ChevronDownIcon, ChevronUpIcon, CopyIcon, + Crosshair1Icon, PlayIcon, ReloadIcon, } from "@radix-ui/react-icons"; @@ -18,8 +19,11 @@ import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery"; import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle"; import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation"; import { convert } from "./workflowEditorUtils"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; type Props = { + debuggableBlockCount: number; title: string; parametersPanelOpen: boolean; onParametersClick: () => void; @@ -29,6 +33,7 @@ type Props = { }; function WorkflowHeader({ + debuggableBlockCount, title, parametersPanelOpen, onParametersClick, @@ -36,10 +41,13 @@ function WorkflowHeader({ onTitleChange, saving, }: Props) { - const { workflowPermanentId } = useParams(); + const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams(); const { data: globalWorkflows } = useGlobalWorkflowsQuery(); const navigate = useNavigate(); const createWorkflowMutation = useCreateWorkflowMutation(); + const debugStore = useDebugStore(); + const anyBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel.length > 0; if (!globalWorkflows) { return null; // this should be loaded already by some other components @@ -50,7 +58,11 @@ function WorkflowHeader({ ); return ( -
+
) : ( <> + @@ -115,15 +142,17 @@ function WorkflowHeader({ )} - + {!debugStore.isDebugMode && ( + + )} )}
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx index 88721c3e..dac8cb12 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx @@ -6,8 +6,6 @@ import { } from "@/components/ui/accordion"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; import { Handle, NodeProps, @@ -17,8 +15,6 @@ import { useReactFlow, } from "@xyflow/react"; import { useState } from "react"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import type { ActionNode } from "./types"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Checkbox } from "@/components/ui/checkbox"; @@ -28,14 +24,16 @@ import { Switch } from "@/components/ui/switch"; import { placeholders, helpTooltips } from "../../helpContent"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { AppNode } from ".."; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { useParams } from "react-router-dom"; +import { NodeHeader } from "../components/NodeHeader"; const urlTooltip = "The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off."; @@ -44,13 +42,9 @@ const navigationGoalTooltip = const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.'; -function ActionNode({ id, data }: NodeProps) { +function ActionNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { editable, debuggable, label } = data; const [inputs, setInputs] = useState({ url: data.url, navigationGoal: data.navigationGoal, @@ -64,7 +58,11 @@ function ActionNode({ id, data }: NodeProps) { totpIdentifier: data.totpIdentifier, engine: data.engine, }); - const deleteNodeCallback = useDeleteNodeCallback(); + const { blockLabel: urlBlockLabel } = useParams(); + const debugStore = useDebugStore(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; + const elideFromDebugging = debugStore.isDebugMode && !debuggable; const nodes = useNodes(); const edges = useEdges(); @@ -94,33 +92,29 @@ function ActionNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Action Block -
-
- { - deleteNodeCallback(id); - }} - /> -
-
+
+ +
@@ -169,7 +163,13 @@ function ActionNode({ id, data }: NodeProps) {
- + Advanced Settings diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/types.ts index d54cfe95..1ce012f6 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type ActionNodeData = NodeBaseData & { url: string; @@ -19,6 +20,7 @@ export type ActionNodeData = NodeBaseData & { export type ActionNode = Node; export const actionNodeDefaultData: ActionNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("action"), label: "", url: "", navigationGoal: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx index 021b5709..f3d79054 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx @@ -1,23 +1,22 @@ import { Label } from "@/components/ui/label"; import { WorkflowBlockInputSet } from "@/components/WorkflowBlockInputSet"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { CodeBlockNode } from "./types"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function CodeBlockNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const deleteNodeCallback = useDeleteNodeCallback(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ code: data.code, parameterKeys: data.parameterKeys, @@ -37,32 +36,24 @@ function CodeBlockNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Code Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
) { - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; return (
@@ -32,32 +31,24 @@ function DownloadNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Download Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/types.ts index a22aaae7..2aaf80ce 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { SKYVERN_DOWNLOAD_DIRECTORY } from "../../constants"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type DownloadNodeData = NodeBaseData & { url: string; @@ -9,6 +10,7 @@ export type DownloadNodeData = NodeBaseData & { export type DownloadNode = Node; export const downloadNodeDefaultData: DownloadNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("download_to_s3"), editable: true, label: "", url: SKYVERN_DOWNLOAD_DIRECTORY, diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx index 67f8ac2e..d53f529a 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx @@ -1,5 +1,4 @@ import { HelpTooltip } from "@/components/HelpTooltip"; -import { ExtractIcon } from "@/components/icons/ExtractIcon"; import { Accordion, AccordionContent, @@ -10,8 +9,6 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; import { Handle, NodeProps, @@ -21,8 +18,6 @@ import { useReactFlow, } from "@xyflow/react"; import { useState } from "react"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { dataSchemaExampleValue } from "../types"; import type { ExtractionNode } from "./types"; @@ -35,14 +30,19 @@ import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; -function ExtractionNode({ id, data }: NodeProps) { +function ExtractionNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ url: data.url, dataExtractionGoal: data.dataExtractionGoal, @@ -53,7 +53,6 @@ function ExtractionNode({ id, data }: NodeProps) { engine: data.engine, model: data.model, }); - const deleteNodeCallback = useDeleteNodeCallback(); const nodes = useNodes(); const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); @@ -82,29 +81,24 @@ function ExtractionNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Extraction Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts index c78d2c16..2045bf50 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type ExtractionNodeData = NodeBaseData & { url: string; @@ -16,6 +17,7 @@ export type ExtractionNodeData = NodeBaseData & { export type ExtractionNode = Node; export const extractionNodeDefaultData: ExtractionNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("extraction"), label: "", url: "", dataExtractionGoal: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx index c7888ff4..e615a638 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx @@ -12,9 +12,6 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { DownloadIcon } from "@radix-ui/react-icons"; import { Handle, NodeProps, @@ -25,8 +22,6 @@ import { } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips, placeholders } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { errorMappingExampleValue } from "../types"; import type { FileDownloadNode } from "./types"; import { AppNode } from ".."; @@ -35,6 +30,10 @@ import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; const urlTooltip = "The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off."; @@ -45,11 +44,12 @@ const navigationGoalPlaceholder = "Tell Skyvern which file to download."; function FileDownloadNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; + const elideFromDebugging = debugStore.isDebugMode && !debuggable; const [inputs, setInputs] = useState({ url: data.url, navigationGoal: data.navigationGoal, @@ -63,14 +63,10 @@ function FileDownloadNode({ id, data }: NodeProps) { engine: data.engine, model: data.model, }); - const deleteNodeCallback = useDeleteNodeCallback(); - const nodes = useNodes(); const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); - const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); - function handleChange(key: string, value: unknown) { if (!editable) { return; @@ -93,31 +89,24 @@ function FileDownloadNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - - File Download Block - -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/types.ts index 877211ac..098bad49 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type FileDownloadNodeData = NodeBaseData & { url: string; @@ -19,6 +20,7 @@ export type FileDownloadNodeData = NodeBaseData & { export type FileDownloadNode = Node; export const fileDownloadNodeDefaultData: FileDownloadNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("file_download"), label: "", url: "", navigationGoal: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx index 79f26def..d2f469f8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx @@ -1,28 +1,27 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type FileParserNode } from "./types"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function FileParserNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ fileUrl: data.fileUrl, }); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); @@ -40,32 +39,24 @@ function FileParserNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - File Parser Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/types.ts index 6a44fe59..672b7eeb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/types.ts @@ -1,5 +1,6 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type FileParserNodeData = NodeBaseData & { fileUrl: string; @@ -8,6 +9,7 @@ export type FileParserNodeData = NodeBaseData & { export type FileParserNode = Node; export const fileParserNodeDefaultData: FileParserNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("file_url_parser"), editable: true, label: "", fileUrl: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx index f0a5a4c5..3e22f1e4 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/FileUploadNode.tsx @@ -1,25 +1,24 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type FileUploadNode } from "./types"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { useState } from "react"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function FileUploadNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const deleteNodeCallback = useDeleteNodeCallback(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ storageType: data.storageType, @@ -52,32 +51,24 @@ function FileUploadNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - File Upload Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts index 0efa575f..5ad220e1 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileUploadNode/types.ts @@ -1,5 +1,6 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type FileUploadNodeData = NodeBaseData & { path: string; @@ -14,6 +15,7 @@ export type FileUploadNodeData = NodeBaseData & { export type FileUploadNode = Node; export const fileUploadNodeDefaultData: FileUploadNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("upload_to_s3"), editable: true, storageType: "s3", label: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx index d91ca53c..6ad04fdb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx @@ -12,9 +12,6 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, @@ -25,10 +22,7 @@ import { } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips, placeholders } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { errorMappingExampleValue } from "../types"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { LoginNode } from "./types"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { AppNode } from ".."; @@ -37,14 +31,19 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow" import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; -function LoginNode({ id, data }: NodeProps) { +function LoginNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ url: data.url, navigationGoal: data.navigationGoal, @@ -59,7 +58,6 @@ function LoginNode({ id, data }: NodeProps) { engine: data.engine, model: data.model, }); - const deleteNodeCallback = useDeleteNodeCallback(); const nodes = useNodes(); const edges = useEdges(); @@ -88,32 +86,24 @@ function LoginNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Login Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/types.ts index 4ab53473..8ea5a61f 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type LoginNodeData = NodeBaseData & { url: string; @@ -20,6 +21,7 @@ export type LoginNodeData = NodeBaseData & { export type LoginNode = Node; export const loginNodeDefaultData: LoginNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("login"), label: "", url: "", navigationGoal: diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx index 7b65881c..417b1c38 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx @@ -1,9 +1,6 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import type { Node } from "@xyflow/react"; import { Handle, @@ -14,14 +11,15 @@ import { } from "@xyflow/react"; import { AppNode } from ".."; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { LoopNode } from "./types"; import { useState } from "react"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { Checkbox } from "@/components/ui/checkbox"; import { getLoopNodeWidth } from "../../workflowEditorUtils"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function LoopNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); @@ -30,14 +28,15 @@ function LoopNode({ id, data }: NodeProps) { if (!node) { throw new Error("Node not found"); // not possible } - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ loopVariableReference: data.loopVariableReference, }); - const deleteNodeCallback = useDeleteNodeCallback(); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); @@ -91,32 +90,24 @@ function LoopNode({ id, data }: NodeProps) { }} >
-
-
-
-
- -
-
- - Loop Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/types.ts index e2843fa4..8f24959e 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/types.ts @@ -1,5 +1,6 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type LoopNodeData = NodeBaseData & { loopValue: string; @@ -10,6 +11,7 @@ export type LoopNodeData = NodeBaseData & { export type LoopNode = Node; export const loopNodeDefaultData: LoopNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("for_loop"), editable: true, label: "", loopValue: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx index 01cb8383..95e5ee6d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/NavigationNode.tsx @@ -13,9 +13,6 @@ import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, @@ -26,10 +23,7 @@ import { } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips, placeholders } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { errorMappingExampleValue } from "../types"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { NavigationNode } from "./types"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { AppNode } from ".."; @@ -37,32 +31,36 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { useParams } from "react-router-dom"; +import { NodeHeader } from "../components/NodeHeader"; -function NavigationNode({ id, data }: NodeProps) { +function NavigationNode({ id, data, type }: NodeProps) { + const { blockLabel: urlBlockLabel } = useParams(); + const debugStore = useDebugStore(); const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { editable, debuggable, label } = data; + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; + const elideFromDebugging = debugStore.isDebugMode && !debuggable; const [inputs, setInputs] = useState({ - url: data.url, - navigationGoal: data.navigationGoal, - errorCodeMapping: data.errorCodeMapping, - maxStepsOverride: data.maxStepsOverride, allowDownloads: data.allowDownloads, - continueOnFailure: data.continueOnFailure, cacheActions: data.cacheActions, - downloadSuffix: data.downloadSuffix, - totpVerificationUrl: data.totpVerificationUrl, - totpIdentifier: data.totpIdentifier, completeCriterion: data.completeCriterion, - terminateCriterion: data.terminateCriterion, + continueOnFailure: data.continueOnFailure, + downloadSuffix: data.downloadSuffix, engine: data.engine, - model: data.model, + errorCodeMapping: data.errorCodeMapping, includeActionHistoryInVerification: data.includeActionHistoryInVerification, + maxStepsOverride: data.maxStepsOverride, + model: data.model, + navigationGoal: data.navigationGoal, + terminateCriterion: data.terminateCriterion, + totpIdentifier: data.totpIdentifier, + totpVerificationUrl: data.totpVerificationUrl, + url: data.url, }); - const deleteNodeCallback = useDeleteNodeCallback(); const nodes = useNodes(); const edges = useEdges(); @@ -92,33 +90,29 @@ function NavigationNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Navigation Block -
-
- { - deleteNodeCallback(id); - }} - /> -
-
+
+ +
@@ -170,7 +164,13 @@ function NavigationNode({ id, data }: NodeProps) {
- + Advanced Settings diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts index 8c6e2291..187e8204 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NavigationNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type NavigationNodeData = NodeBaseData & { url: string; @@ -23,6 +24,7 @@ export type NavigationNodeData = NodeBaseData & { export type NavigationNode = Node; export const navigationNodeDefaultData: NavigationNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("navigation"), label: "", url: "", navigationGoal: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/PDFParserNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/PDFParserNode.tsx index f243d17c..d4edf73c 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/PDFParserNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/PDFParserNode.tsx @@ -1,31 +1,30 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { dataSchemaExampleForFileExtraction } from "../types"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type PDFParserNode } from "./types"; import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function PDFParserNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ fileUrl: data.fileUrl, jsonSchema: data.jsonSchema, }); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); function handleChange(key: string, value: unknown) { if (!data.editable) { @@ -51,32 +50,24 @@ function PDFParserNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - PDF Parser Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/types.ts index 0edf685e..c1c51220 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/PDFParserNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { AppNode } from ".."; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type PDFParserNodeData = NodeBaseData & { fileUrl: string; @@ -10,6 +11,7 @@ export type PDFParserNodeData = NodeBaseData & { export type PDFParserNode = Node; export const pdfParserNodeDefaultData: PDFParserNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("pdf_parser"), editable: true, label: "", fileUrl: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx index 0e33bcd0..e1e8860d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx @@ -1,27 +1,26 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type SendEmailNode } from "./types"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function SendEmailNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ recipients: data.recipients, subject: data.subject, @@ -53,32 +52,24 @@ function SendEmailNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Send Email Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/types.ts index 62e6ffbf..be7ef521 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/types.ts @@ -8,6 +8,7 @@ import { SMTP_USERNAME_PARAMETER_KEY, } from "../../constants"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type SendEmailNodeData = NodeBaseData & { recipients: string; @@ -24,6 +25,7 @@ export type SendEmailNodeData = NodeBaseData & { export type SendEmailNode = Node; export const sendEmailNodeDefaultData: SendEmailNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("send_email"), recipients: "", subject: "", body: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx index 61bb961b..8a33f48f 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx @@ -7,7 +7,7 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { ProxyLocation } from "@/api/types"; import { useQuery } from "@tanstack/react-query"; import { Label } from "@/components/ui/label"; @@ -22,8 +22,10 @@ import { ModelSelector } from "@/components/ModelSelector"; import { WorkflowModel } from "@/routes/workflows/types/workflowTypes"; import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "../Taskv2Node/types"; import { KeyValueInput } from "@/components/KeyValueInput"; +import { useWorkflowSettingsStore } from "@/store/WorkflowSettingsStore"; function StartNode({ id, data }: NodeProps) { + const workflowSettingsStore = useWorkflowSettingsStore(); const credentialGetter = useCredentialGetter(); const { updateNodeData } = useReactFlow(); @@ -59,6 +61,11 @@ function StartNode({ id, data }: NodeProps) { extraHttpHeaders: data.withWorkflowSettings ? data.extraHttpHeaders : null, }); + useEffect(() => { + workflowSettingsStore.setWorkflowSettings(inputs); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputs]); + function handleChange(key: string, value: unknown) { if (!data.editable) { return; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx index 82808f2a..275013d3 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx @@ -13,9 +13,6 @@ import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, @@ -28,31 +25,31 @@ import { useState } from "react"; import { AppNode } from ".."; import { helpTooltips, placeholders } from "../../helpContent"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { dataSchemaExampleValue, errorMappingExampleValue } from "../types"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { ParametersMultiSelect } from "./ParametersMultiSelect"; import type { TaskNode } from "./types"; import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; -function TaskNode({ id, data }: NodeProps) { +function TaskNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const nodes = useNodes(); const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); - const [inputs, setInputs] = useState({ url: data.url, navigationGoal: data.navigationGoal, @@ -95,32 +92,24 @@ function TaskNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Task Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+ Content diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts index 2180dca8..3da6ef34 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/types.ts @@ -1,7 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { RunEngine } from "@/api/types"; - +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type TaskNodeData = NodeBaseData & { url: string; navigationGoal: string; @@ -25,6 +25,7 @@ export type TaskNodeData = NodeBaseData & { export type TaskNode = Node; export const taskNodeDefaultData: TaskNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("task"), url: "", navigationGoal: "", dataExtractionGoal: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx index f4defd45..8eebbc3a 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx @@ -9,28 +9,25 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips, placeholders } from "../../helpContent"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function Taskv2Node({ id, data, type }: NodeProps) { + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const { updateNodeData } = useReactFlow(); - const { editable } = data; - const deleteNodeCallback = useDeleteNodeCallback(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); - const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const [inputs, setInputs] = useState({ @@ -64,34 +61,24 @@ function Taskv2Node({ id, data, type }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - - Navigation v2 Block - -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/types.ts index 6a638a65..f701d9ca 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/types.ts @@ -1,5 +1,6 @@ import { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export const MAX_STEPS_DEFAULT = 25; export const MAX_SCREENSHOT_SCROLLS_DEFAULT = 3; @@ -16,6 +17,7 @@ export type Taskv2NodeData = NodeBaseData & { export type Taskv2Node = Node; export const taskv2NodeDefaultData: Taskv2NodeData = { + debuggable: debuggableWorkflowBlockTypes.has("task_v2"), label: "", continueOnFailure: false, editable: true, diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx index 16b16d3d..9c421be9 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx @@ -1,9 +1,6 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, @@ -16,21 +13,26 @@ import { useState } from "react"; import { AppNode } from ".."; import { helpTooltips } from "../../helpContent"; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type TextPromptNode } from "./types"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup"; import { dataSchemaExampleValue } from "../types"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function TextPromptNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const deleteNodeCallback = useDeleteNodeCallback(); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ prompt: data.prompt, jsonSchema: data.jsonSchema, @@ -41,11 +43,6 @@ function TextPromptNode({ id, data }: NodeProps) { const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); - function handleChange(key: string, value: unknown) { if (!editable) { return; @@ -70,32 +67,24 @@ function TextPromptNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Text Prompt Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/types.ts index 29806fc0..8733e7f1 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; import { AppNode } from ".."; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type TextPromptNodeData = NodeBaseData & { prompt: string; @@ -11,6 +12,7 @@ export type TextPromptNodeData = NodeBaseData & { export type TextPromptNode = Node; export const textPromptNodeDefaultData: TextPromptNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("text_prompt"), editable: true, label: "", prompt: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx index 02cbae5d..76667059 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx @@ -1,25 +1,23 @@ import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import type { URLNode } from "./types"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; import { useState } from "react"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { Label } from "@/components/ui/label"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { placeholders } from "../../helpContent"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function URLNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const deleteNodeCallback = useDeleteNodeCallback(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); const [inputs, setInputs] = useState({ @@ -48,32 +46,24 @@ function URLNode({ id, data, type }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Go to URL Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts index 94c81bca..1f38bcf0 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts @@ -1,5 +1,6 @@ import { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type URLNodeData = NodeBaseData & { url: string; @@ -8,6 +9,7 @@ export type URLNodeData = NodeBaseData & { export type URLNode = Node; export const urlNodeDefaultData: URLNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("goto_url"), label: "", continueOnFailure: false, url: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx index 8d479b30..0de2f7f1 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx @@ -1,22 +1,21 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position } from "@xyflow/react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import { type UploadNode } from "./types"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; function UploadNode({ id, data }: NodeProps) { - const deleteNodeCallback = useDeleteNodeCallback(); - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; return (
@@ -32,32 +31,24 @@ function UploadNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Upload Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/types.ts index b0529dca..ccba96f8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/types.ts @@ -1,6 +1,7 @@ import type { Node } from "@xyflow/react"; import { SKYVERN_DOWNLOAD_DIRECTORY } from "../../constants"; import { NodeBaseData } from "../types"; +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type UploadNodeData = NodeBaseData & { path: string; @@ -10,6 +11,7 @@ export type UploadNodeData = NodeBaseData & { export type UploadNode = Node; export const uploadNodeDefaultData: UploadNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("file_upload"), editable: true, label: "", path: SKYVERN_DOWNLOAD_DIRECTORY, diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx index aa01fba6..c8ba95ad 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx @@ -11,9 +11,6 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, @@ -24,31 +21,32 @@ import { } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; import { errorMappingExampleValue } from "../types"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { ValidationNode } from "./types"; import { AppNode } from ".."; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { ModelSelector } from "@/components/ModelSelector"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; -function ValidationNode({ id, data }: NodeProps) { +function ValidationNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ completeCriterion: data.completeCriterion, terminateCriterion: data.terminateCriterion, errorCodeMapping: data.errorCodeMapping, model: data.model, }); - const deleteNodeCallback = useDeleteNodeCallback(); const nodes = useNodes(); const edges = useEdges(); const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id); @@ -77,32 +75,24 @@ function ValidationNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Validation Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/types.ts index 20c869cd..13a41fba 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/types.ts @@ -1,6 +1,6 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; - +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type ValidationNodeData = NodeBaseData & { completeCriterion: string; terminateCriterion: string; @@ -11,6 +11,7 @@ export type ValidationNodeData = NodeBaseData & { export type ValidationNode = Node; export const validationNodeDefaultData: ValidationNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("validation"), label: "", completeCriterion: "", terminateCriterion: "", diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/WaitNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/WaitNode.tsx index c1381458..9bc27ad5 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/WaitNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/WaitNode.tsx @@ -1,29 +1,27 @@ import { HelpTooltip } from "@/components/HelpTooltip"; import { Label } from "@/components/ui/label"; -import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; -import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; -import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { useState } from "react"; import { helpTooltips } from "../../helpContent"; -import { EditableNodeTitle } from "../components/EditableNodeTitle"; -import { NodeActionMenu } from "../NodeActionMenu"; -import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; import type { WaitNode } from "./types"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { Input } from "@/components/ui/input"; +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; +import { NodeHeader } from "../components/NodeHeader"; +import { useParams } from "react-router-dom"; -function WaitNode({ id, data }: NodeProps) { +function WaitNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); - const { editable } = data; - const [label, setLabel] = useNodeLabelChangeHandler({ - id, - initialValue: data.label, - }); + const { debuggable, editable, label } = data; + const debugStore = useDebugStore(); + const elideFromDebugging = debugStore.isDebugMode && !debuggable; + const { blockLabel: urlBlockLabel } = useParams(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === label; const [inputs, setInputs] = useState({ waitInSeconds: data.waitInSeconds, }); - const deleteNodeCallback = useDeleteNodeCallback(); function handleChange(key: string, value: unknown) { if (!editable) { @@ -49,32 +47,24 @@ function WaitNode({ id, data }: NodeProps) { id="b" className="opacity-0" /> -
-
-
-
- -
-
- - Wait Block -
-
- { - deleteNodeCallback(id); - }} - /> -
+
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/types.ts index 8fe5c833..90ab9747 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WaitNode/types.ts @@ -1,6 +1,6 @@ import type { Node } from "@xyflow/react"; import { NodeBaseData } from "../types"; - +import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; export type WaitNodeData = NodeBaseData & { waitInSeconds: string; }; @@ -8,6 +8,7 @@ export type WaitNodeData = NodeBaseData & { export type WaitNode = Node; export const waitNodeDefaultData: WaitNodeData = { + debuggable: debuggableWorkflowBlockTypes.has("wait"), label: "", continueOnFailure: false, editable: true, diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx new file mode 100644 index 00000000..89c3c950 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx @@ -0,0 +1,391 @@ +import { AxiosError } from "axios"; +import { ReloadIcon, PlayIcon, StopIcon } from "@radix-ui/react-icons"; +import { useEffect } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { getClient } from "@/api/AxiosClient"; +import { ProxyLocation } from "@/api/types"; +import { Timer } from "@/components/Timer"; +import { toast } from "@/components/ui/use-toast"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; +import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; +import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; +import { + debuggableWorkflowBlockTypes, + type WorkflowBlockType, + type WorkflowApiResponse, +} from "@/routes/workflows/types/workflowTypes"; +import { getInitialValues } from "@/routes/workflows/utils"; +import { useDebugStore } from "@/store/useDebugStore"; +import { + useWorkflowSettingsStore, + type WorkflowSettingsState, +} from "@/store/WorkflowSettingsStore"; +import { cn } from "@/util/utils"; +import { + statusIsAFailureType, + statusIsFinalized, + statusIsRunningOrQueued, +} from "@/routes/tasks/types"; + +import { EditableNodeTitle } from "../components/EditableNodeTitle"; +import { NodeActionMenu } from "../NodeActionMenu"; +import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; +import { lsKeys } from "@/util/env"; + +interface Props { + blockLabel: string; // today, this + wpid act as the identity of a block + disabled?: boolean; + editable: boolean; + nodeId: string; + totpIdentifier: string | null; + totpUrl: string | null; + type: WorkflowBlockType; +} + +type Payload = Record & { + block_labels: string[]; + browser_session_id: string | null; + extra_http_headers: Record | null; + max_screenshot_scrolls: number | null; + parameters: Record; + proxy_location: ProxyLocation; + totp_identifier: string | null; + totp_url: string | null; + webhook_url: string | null; + workflow_id: string; +}; + +const blockTypeToTitle = (type: WorkflowBlockType): string => { + const parts = type.split("_"); + const capCased = parts + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + + return `${capCased} Block`; +}; + +const getPayload = (opts: { + blockLabel: string; + parameters: Record; + totpIdentifier: string | null; + totpUrl: string | null; + workflowPermanentId: string; + workflowSettings: WorkflowSettingsState; +}): Payload => { + const webhook_url = opts.workflowSettings.webhookCallbackUrl.trim(); + + let extraHttpHeaders = null; + + try { + extraHttpHeaders = + opts.workflowSettings.extraHttpHeaders === null + ? null + : JSON.parse(opts.workflowSettings.extraHttpHeaders); + } catch (e: unknown) { + toast({ + variant: "warning", + title: "Extra HTTP Headers", + description: "Invalid extra HTTP Headers JSON", + }); + } + + const stored = localStorage.getItem(lsKeys.optimisticBrowserSession); + let browserSessionId: string | null = null; + try { + const parsed = JSON.parse(stored ?? ""); + const { browser_session_id } = parsed; + browserSessionId = browser_session_id as string; + } catch { + // pass + } + + if (!browserSessionId) { + toast({ + variant: "warning", + title: "Error", + description: "No browser session ID found", + }); + } else { + toast({ + variant: "default", + title: "Success", + description: `Browser session ID found: ${browserSessionId}`, + }); + } + + const payload: Payload = { + block_labels: [opts.blockLabel], + browser_session_id: browserSessionId, + extra_http_headers: extraHttpHeaders, + max_screenshot_scrolls: opts.workflowSettings.maxScreenshotScrollingTimes, + parameters: opts.parameters, + proxy_location: opts.workflowSettings.proxyLocation, + totp_identifier: opts.totpIdentifier, + totp_url: opts.totpUrl, + webhook_url: webhook_url.length > 0 ? webhook_url : null, + workflow_id: opts.workflowPermanentId, + }; + + return payload; +}; + +function NodeHeader({ + blockLabel, + disabled = false, + editable, + nodeId, + totpIdentifier, + totpUrl, + type, +}: Props) { + const { + blockLabel: urlBlockLabel, + workflowPermanentId, + workflowRunId, + } = useParams(); + const debugStore = useDebugStore(); + const thisBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel === blockLabel; + const anyBlockIsPlaying = + urlBlockLabel !== undefined && urlBlockLabel.length > 0; + const workflowSettingsStore = useWorkflowSettingsStore(); + const [label, setLabel] = useNodeLabelChangeHandler({ + id: nodeId, + initialValue: blockLabel, + }); + const blockTitle = blockTypeToTitle(type); + const deleteNodeCallback = useDeleteNodeCallback(); + const credentialGetter = useCredentialGetter(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const location = useLocation(); + const isDebuggable = debuggableWorkflowBlockTypes.has(type); + const { data: workflowRun } = useWorkflowRunQuery(); + const workflowRunIsRunningOrQueued = + workflowRun && statusIsRunningOrQueued(workflowRun); + + useEffect(() => { + if (!workflowRun || !workflowPermanentId || !workflowRunId) { + return; + } + + if ( + workflowRunId === workflowRun?.workflow_run_id && + statusIsFinalized(workflowRun) + ) { + navigate(`/workflows/${workflowPermanentId}/debug`); + + if (statusIsAFailureType(workflowRun)) { + toast({ + variant: "destructive", + title: `Workflow Block ${urlBlockLabel}: ${workflowRun.status}`, + description: `Reason: ${workflowRun.failure_reason}`, + }); + } else if (statusIsFinalized(workflowRun)) { + toast({ + variant: "success", + title: `Workflow Block ${urlBlockLabel}: ${workflowRun.status}`, + }); + } + } + }, [ + urlBlockLabel, + navigate, + workflowPermanentId, + workflowRun, + workflowRunId, + ]); + + const runBlock = useMutation({ + mutationFn: async () => { + if (!workflowPermanentId) { + console.error("There is no workflowPermanentId"); + return; + } + + const workflow = await queryClient.fetchQuery({ + queryKey: ["block", "workflow", workflowPermanentId], + queryFn: async () => { + const client = await getClient(credentialGetter); + return client + .get(`/workflows/${workflowPermanentId}`) + .then((response) => response.data); + }, + }); + + const workflowParameters = + workflow?.workflow_definition.parameters.filter( + (parameter) => parameter.parameter_type === "workflow", + ); + + const parameters = getInitialValues(location, workflowParameters ?? []); + + const client = await getClient(credentialGetter, "sans-api-v1"); + + const body = getPayload({ + blockLabel, + parameters, + totpIdentifier, + totpUrl, + workflowPermanentId, + workflowSettings: workflowSettingsStore, + }); + + return await client.post( + "/run/workflows/blocks", + body, + ); + }, + onSuccess: (response) => { + if (!response) { + console.error("No response"); + return; + } + + toast({ + variant: "success", + title: "Workflow block run started", + description: "The workflow block run has been started successfully", + }); + + navigate( + `/workflows/${workflowPermanentId}/${response.data.run_id}/${label}/debug`, + ); + }, + onError: (error: AxiosError) => { + const detail = (error.response?.data as { detail?: string })?.detail; + toast({ + variant: "destructive", + title: "Failed to start workflow block run", + description: detail ?? error.message, + }); + }, + }); + + const cancelBlock = useMutation({ + mutationFn: async () => { + const client = await getClient(credentialGetter); + return client + .post(`/workflows/runs/${workflowRunId}/cancel`) + .then((response) => response.data); + }, + onSuccess: () => { + toast({ + variant: "success", + title: "Workflow Canceled", + description: "The workflow has been successfully canceled.", + }); + navigate(`/workflows/${workflowPermanentId}/debug`); + }, + onError: (error) => { + toast({ + variant: "destructive", + title: "Error", + description: error.message, + }); + }, + }); + + const handleOnPlay = () => { + runBlock.mutate(); + }; + + const handleOnCancel = () => { + cancelBlock.mutate(); + }; + + return ( + <> + {thisBlockIsPlaying && ( +
+
+ +
+
{workflowRun?.status ?? "pending"}
+
+ )} + +
+
+
+ +
+
+ + {blockTitle} +
+
+
+ {thisBlockIsPlaying && workflowRunIsRunningOrQueued && ( +
+ +
+ )} + {debugStore.isDebugMode && isDebuggable && ( + + )} + {disabled || debugStore.isDebugMode ? null : ( +
+
+ { + deleteNodeCallback(nodeId); + }} + /> +
+
+ )} +
+
+ + ); +} + +export { NodeHeader }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts index 7d772afc..16e0f6ce 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts @@ -2,6 +2,7 @@ import { WorkflowBlockType } from "../../types/workflowTypes"; import type { WorkflowModel } from "../../types/workflowTypes"; export type NodeBaseData = { + debuggable: boolean; label: string; continueOnFailure: boolean; editable: boolean; diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowDebugOverviewWindow.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowDebugOverviewWindow.tsx new file mode 100644 index 00000000..5b82ba4d --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowDebugOverviewWindow.tsx @@ -0,0 +1,392 @@ +/** + * NOTE(jdo): this is not a "panel", in the react-flow sense. It's a floating, + * draggable, resizeable window, like on a desktop. But I am putting it here + * for now. + */ + +import { Resizable } from "re-resizable"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import { flushSync } from "react-dom"; +import Draggable from "react-draggable"; +import { useParams } from "react-router-dom"; + +import { useDebugStore } from "@/store/useDebugStore"; +import { cn } from "@/util/utils"; + +/** + * TODO(jdo): extract this to a reusable Window component. + */ +function WorkflowDebugOverviewWindow() { + const debugStore = useDebugStore(); + const isDebugMode = debugStore.isDebugMode; + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [size, setSize] = useState({ + left: 0, + top: 0, + width: 800, + height: 680, + }); + const [lastSize, setLastSize] = useState({ + left: 0, + top: 0, + width: 800, + height: 680, + }); + const [sizeBeforeMaximize, setSizeBeforeMaximize] = useState({ + left: 0, + top: 0, + width: 800, + height: 680, + }); + const [isMaximized, setIsMaximized] = useState(false); + const parentRef = useRef(null); + const resizableRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const [dragStartSize, setDragStartSize] = useState< + | { + left: number; + top: number; + width: number; + height: number; + } + | undefined + >(undefined); + + const onResize = useCallback( + ({ + delta, + direction, + size, + }: { + delta: { width: number; height: number }; + direction: string; + size: { left: number; top: number; width: number; height: number }; + }) => { + if (!dragStartSize) { + return; + } + + const top = + resizableRef.current?.parentElement?.offsetTop ?? lastSize.top; + const left = + resizableRef.current?.parentElement?.offsetLeft ?? lastSize.left; + const width = + resizableRef.current?.parentElement?.offsetWidth ?? lastSize.width; + const height = + resizableRef.current?.parentElement?.offsetHeight ?? lastSize.height; + + setLastSize({ top, left, width, height }); + const directions = ["top", "left", "topLeft", "bottomLeft", "topRight"]; + + if (directions.indexOf(direction) !== -1) { + let newLeft = size.left; + let newTop = size.top; + + if (direction === "bottomLeft") { + newLeft = dragStartSize.left - delta.width; + } else if (direction === "topRight") { + newTop = dragStartSize.top - delta.height; + } else { + newLeft = dragStartSize.left - delta.width; + newTop = dragStartSize.top - delta.height; + } + + // TODO(follow-up): https://github.com/bokuweb/re-resizable/issues/868 + flushSync(() => { + setSize({ + ...size, + left: newLeft, + top: newTop, + }); + setPosition({ x: newLeft, y: newTop }); + }); + } else { + flushSync(() => { + setSize({ + ...size, + left: size.left, + top: size.top, + }); + setPosition({ x: size.left, y: size.top }); + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [dragStartSize], + ); + + /** + * Forces the sizing to take place after the resize is complete. + * + * TODO(jdo): emits warnings in the dev console. ref: https://github.com/bokuweb/re-resizable/issues/868 + */ + useEffect(() => { + if (isResizing) { + return; + } + const width = lastSize.width; + const height = lastSize.height; + + flushSync(() => { + setSize({ + ...size, + width, + height, + }); + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isResizing]); + + const onDrag = (position: { x: number; y: number }) => { + if (isMaximized) { + restore(); + return; + } + + setPosition({ x: position.x, y: position.y }); + + setSize({ + ...size, + left: position.x, + top: position.y, + }); + + setLastSize({ + ...size, + left: position.x, + top: position.y, + }); + }; + + const onDblClickHeader = () => { + if (!isMaximized) { + maximize(); + } else { + restore(); + } + }; + + const maximize = () => { + const parent = parentRef.current; + + if (!parent) { + console.warn("No parent - cannot maximize."); + return; + } + + setSizeBeforeMaximize({ + ...size, + left: position.x, + top: position.y, + }); + + setIsMaximized(true); + + setSize({ + left: 0, + top: 0, + // has to take into account padding...hack + width: parent.offsetWidth - 16, + height: parent.offsetHeight - 16, + }); + + setPosition({ x: 0, y: 0 }); + }; + + const restore = () => { + const restoreSize = sizeBeforeMaximize; + + const position = isDragging + ? { left: 0, top: 0 } + : { + left: restoreSize.left, + top: restoreSize.top, + }; + + setSize({ + left: position.left, + top: position.top, + width: restoreSize.width, + height: restoreSize.height, + }); + + setPosition({ x: position.left, y: position.top }); + + setIsMaximized(false); + }; + + /** + * If maximized, need to retain max size during parent resizing. + */ + useLayoutEffect(() => { + const observer = new ResizeObserver(() => { + const parent = parentRef.current; + + if (!parent) { + return; + } + + if (isMaximized) { + setSize({ + left: 0, + top: 0, + // has to take into account padding...hack + width: parent.offsetWidth - 16, + height: parent.offsetHeight - 16, + }); + } + }); + + observer.observe(parentRef.current!); + + return () => { + observer.disconnect(); + }; + }, [isMaximized]); + + return !isDebugMode ? null : ( +
+ setIsDragging(true)} + onDrag={(_, data) => onDrag(data)} + onStop={() => setIsDragging(false)} + bounds="parent" + disabled={isResizing} + > + { + if (isMaximized) { + return; + } + + setIsResizing(true); + setDragStartSize({ ...size, left: position.x, top: position.y }); + }} + onResize={(_, direction, __, delta) => { + if (isMaximized) { + return; + } + + onResize({ delta, direction, size }); + }} + onResizeStop={() => { + if (isMaximized) { + return; + } + + setIsResizing(false); + setDragStartSize(undefined); + }} + defaultSize={size} + size={size} + > +
{ + onDblClickHeader(); + }} + > +
+ Live View +
+ +
+
+
+
+ ); +} + +function WorkflowDebugOverviewWindowIframe() { + const { workflowPermanentId: wpid, workflowRunId: wrid } = useParams(); + const lastCompletePair = useRef<{ wpid: string; wrid: string } | null>(null); + + if (wpid !== undefined && wrid !== undefined) { + lastCompletePair.current = { + wpid, + wrid, + }; + } + + const paramsToUse = useMemo(() => { + if (wpid && wrid) { + return { wpid, wrid }; + } + return lastCompletePair.current; + }, [wpid, wrid]); + + const origin = location.origin; + const dest = paramsToUse + ? `${origin}/workflows/${paramsToUse.wpid}/${paramsToUse.wrid}/overview?embed=true` + : null; + + return dest ? ( +
+