From 68a88706f78c6d1c49fde0540a0c85b6ebedd2b9 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Sat, 6 Sep 2025 13:49:54 -0700 Subject: [PATCH] use Splitter for debugger layout (#3383) --- skyvern-frontend/src/components/Splitter.tsx | 153 +++++++- .../routes/workflows/editor/FlowRenderer.tsx | 28 +- .../src/routes/workflows/editor/Workspace.tsx | 348 +++++++++--------- .../editor/reactFlowOverrideStyles.css | 5 + 4 files changed, 347 insertions(+), 187 deletions(-) diff --git a/skyvern-frontend/src/components/Splitter.tsx b/skyvern-frontend/src/components/Splitter.tsx index f8483fac..74fc7f2e 100644 --- a/skyvern-frontend/src/components/Splitter.tsx +++ b/skyvern-frontend/src/components/Splitter.tsx @@ -3,7 +3,71 @@ import { useMountEffect } from "@/hooks/useMountEffect"; import { cn } from "@/util/utils"; import { useOnChange } from "@/hooks/useOnChange"; +function Handle({ + direction, + isDragging, + onDoubleClick, +}: { + direction: "vertical" | "horizontal"; + isDragging: boolean; + onDoubleClick?: () => void; +}) { + return ( +
onDoubleClick?.()} + > +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + interface Props { + className?: string; + classNameLeft?: string; + classNameRight?: string; /** * The direction of the splitter. If "vertical", the split bar is vertical, * etc. @@ -38,6 +102,10 @@ interface Props { * key. */ storageKey?: string; + /** + * Callback fired when the splitter is resized + */ + onResize?: () => void; } type SizingTarget = "left" | "right" | "top" | "bottom"; @@ -196,7 +264,16 @@ const setStoredSizing = ( localStorage.setItem(key, sizing); }; -function Splitter({ children, direction, split, storageKey }: Props) { +function Splitter({ + children, + className, + classNameLeft, + classNameRight, + direction, + split, + storageKey, + onResize, +}: Props) { if (!Array.isArray(children) || children.length !== 2) { throw new Error("Splitter must have exactly two children"); } @@ -240,7 +317,9 @@ function Splitter({ children, direction, split, storageKey }: Props) { const newWidth = (newPixelPos / maxSize) * 100; const clampedWidth = Math.max(0, Math.min(newWidth, 100)); + setIsClosed(false); setSplitPosition(clampedWidth); + onResize?.(); }; const onMouseUp = () => { @@ -248,6 +327,7 @@ function Splitter({ children, direction, split, storageKey }: Props) { document.body.classList.remove("no-select-global"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); + onResize?.(); }; document.addEventListener("mousemove", onMouseMove); @@ -255,6 +335,9 @@ function Splitter({ children, direction, split, storageKey }: Props) { }; const [splitPosition, setSplitPosition] = useState(50); + const [isClosed, setIsClosed] = useState(false); + const [closedSplitPosition, setClosedSplitPosition] = + useState(splitPosition); const [isDragging, setIsDragging] = useState(false); useMountEffect(() => { @@ -287,21 +370,51 @@ function Splitter({ children, direction, split, storageKey }: Props) { } }); + /** + * A "snap" is: + * - if the splitter is not "closed", then "close it" + * - if the splitter is "closed", then "open it" + * + * "closed" depends on the `split` prop definition. For instance, if the + * `split` prop has `left` defined, then "closing" is the `left` side + * resizing down to 0. + * + * When "closing", the current splitPosition should be memorized and then + * returned to when an "open" happens. + */ + const snap = () => { + if (isClosed) { + setSplitPosition(closedSplitPosition); + } else { + setClosedSplitPosition(splitPosition); + setSplitPosition(0); + } + setIsClosed(!isClosed); + onResize?.(); + }; + return (
{direction === "vertical" ? ( <>
+ onDoubleClick={snap} + > +
+ +
; edges: Array; setNodes: (nodes: Array) => void; @@ -233,10 +234,12 @@ type Props = { onDebuggableBlockCountChange?: (count: number) => void; onMouseDownCapture?: () => void; zIndex?: number; + onContainerResize?: number; }; function FlowRenderer({ hideBackground = false, + hideControls = false, nodes, edges, setNodes, @@ -249,6 +252,7 @@ function FlowRenderer({ onDebuggableBlockCountChange, onMouseDownCapture, zIndex, + onContainerResize, }: Props) { const reactFlowInstance = useReactFlow(); const debugStore = useDebugStore(); @@ -485,14 +489,34 @@ function FlowRenderer({ useAutoPan(editorElementRef, nodes); + useEffect(() => { + doLayout(nodes, edges); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onContainerResize]); + const zoomLock = 1 as const; const yLockMax = 140 as const; /** * TODO(jdo): hack + * + * Locks the x position of the flow to an ideal x based on the ideal width + * of the flow. The ideal width is based on differently-width'd blocks. */ const getXLock = () => { - return 24; + const rect = editorElementRef.current?.getBoundingClientRect(); + + if (!rect) { + return 24; + } + + const width = rect.width; + const hasLoopBlock = nodes.some((node) => node.type === "loop"); + const hasHttpBlock = nodes.some((node) => node.type === "http_request"); + const idealWidth = hasHttpBlock ? 580 : hasLoopBlock ? 498 : 475; + const split = (width - idealWidth) / 2; + + return Math.max(24, split); }; useOnChange(debugStore.isDebugMode, (newValue) => { @@ -673,7 +697,7 @@ function FlowRenderer({ {!hideBackground && ( )} - + {!hideControls && }
diff --git a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx index ed77b25f..76dcfc9f 100644 --- a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx @@ -28,6 +28,7 @@ import { PowerButton, ReloadButton, } from "@/components/FloatingWindow"; +import { Splitter } from "@/components/Splitter"; import { Dialog, DialogContent, @@ -127,6 +128,7 @@ function Workspace({ const [showPowerButton, setShowPowerButton] = useState(true); const [reloadKey, setReloadKey] = useState(0); const [windowResizeTrigger, setWindowResizeTrigger] = useState(0); + const [containerResizeTrigger, setContainerResizeTrigger] = useState(0); const [isReloading, setIsReloading] = useState(false); const credentialGetter = useCredentialGetter(); const queryClient = useQueryClient(); @@ -206,10 +208,10 @@ function Workspace({ const hasLoopBlock = nodes.some((node) => node.type === "loop"); const hasHttpBlock = nodes.some((node) => node.type === "http_request"); const workflowWidth = hasHttpBlock - ? "39rem" + ? "35.1rem" : hasLoopBlock - ? "34.25rem" - : "33rem"; + ? "31.25rem" + : "30rem"; /** * Open a new tab (not window) with the browser session URL. @@ -762,195 +764,193 @@ function Workspace({ {/* infinite canvas, sub panels, browser, and timeline when in debug mode */} {showBrowser && (
- {/* infinite canvas */} -
setContainerResizeTrigger((prev) => prev + 1)} > - -
+ {/* infinite canvas */} +
+ +
- {/* divider*/} -
- - {/* browser & timeline & sub-panels in debug mode */} -
- {/* sub panels */} - {workflowPanelState.active && ( -
- {workflowPanelState.content === "cacheKeyValues" && ( - { - setToDeleteCacheKeyValue(cacheKeyValue); - setOpenConfirmCacheKeyValueDeleteDialogue(true); - }} - onPaginate={(page) => { - setPage(page); - }} - onSelect={(cacheKeyValue) => { - setCacheKeyValue(cacheKeyValue); - setCacheKeyValueFilter(""); - closeWorkflowPanel(); - }} - /> - )} - {workflowPanelState.content === "parameters" && ( - - )} - {workflowPanelState.content === "nodeLibrary" && ( - { - addNode(props); - }} - /> - )} -
- )} - - {/* browser & timeline */} -
- {/* browser */} -
-
- {activeDebugSession && - activeDebugSession.browser_session_id && - !cycleBrowser.isPending ? ( - - ) : ( -
- Connecting to your browser... - -
- )} -
-
-
- Live Browser -
- {showBreakoutButton && ( - breakout()} /> - )} -
- {showPowerButton && cycle()} />} - reload()} - /> -
-
-
- - {/* timeline */} - )}
diff --git a/skyvern-frontend/src/routes/workflows/editor/reactFlowOverrideStyles.css b/skyvern-frontend/src/routes/workflows/editor/reactFlowOverrideStyles.css index 2b409213..fc320e63 100644 --- a/skyvern-frontend/src/routes/workflows/editor/reactFlowOverrideStyles.css +++ b/skyvern-frontend/src/routes/workflows/editor/reactFlowOverrideStyles.css @@ -21,3 +21,8 @@ .react-flow__handle-bottom { bottom: 3px; } + +.react-flow__attribution { + background-color: rgb(2 6 23); + color: white; +}