use Splitter for debugger layout (#3383)
This commit is contained in:
@@ -3,7 +3,71 @@ import { useMountEffect } from "@/hooks/useMountEffect";
|
|||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import { useOnChange } from "@/hooks/useOnChange";
|
import { useOnChange } from "@/hooks/useOnChange";
|
||||||
|
|
||||||
|
function Handle({
|
||||||
|
direction,
|
||||||
|
isDragging,
|
||||||
|
onDoubleClick,
|
||||||
|
}: {
|
||||||
|
direction: "vertical" | "horizontal";
|
||||||
|
isDragging: boolean;
|
||||||
|
onDoubleClick?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute flex h-[1.25rem] w-[10px] flex-wrap items-center justify-center gap-[2px] bg-slate-800 pb-1 pt-1",
|
||||||
|
{
|
||||||
|
"cursor-col-resize": direction === "vertical",
|
||||||
|
"cursor-row-resize": direction === "horizontal",
|
||||||
|
"bg-slate-700": isDragging,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onDoubleClick={() => onDoubleClick?.()}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-[0.15rem]">
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-center gap-[0.15rem]">
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-center gap-[0.15rem]">
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("h-[2px] w-[2px] rounded-full bg-[#666]", {
|
||||||
|
"bg-[#222]": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
classNameLeft?: string;
|
||||||
|
classNameRight?: string;
|
||||||
/**
|
/**
|
||||||
* The direction of the splitter. If "vertical", the split bar is vertical,
|
* The direction of the splitter. If "vertical", the split bar is vertical,
|
||||||
* etc.
|
* etc.
|
||||||
@@ -38,6 +102,10 @@ interface Props {
|
|||||||
* key.
|
* key.
|
||||||
*/
|
*/
|
||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
|
/**
|
||||||
|
* Callback fired when the splitter is resized
|
||||||
|
*/
|
||||||
|
onResize?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SizingTarget = "left" | "right" | "top" | "bottom";
|
type SizingTarget = "left" | "right" | "top" | "bottom";
|
||||||
@@ -196,7 +264,16 @@ const setStoredSizing = (
|
|||||||
localStorage.setItem(key, sizing);
|
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) {
|
if (!Array.isArray(children) || children.length !== 2) {
|
||||||
throw new Error("Splitter must have exactly two children");
|
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 newWidth = (newPixelPos / maxSize) * 100;
|
||||||
const clampedWidth = Math.max(0, Math.min(newWidth, 100));
|
const clampedWidth = Math.max(0, Math.min(newWidth, 100));
|
||||||
|
|
||||||
|
setIsClosed(false);
|
||||||
setSplitPosition(clampedWidth);
|
setSplitPosition(clampedWidth);
|
||||||
|
onResize?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
@@ -248,6 +327,7 @@ function Splitter({ children, direction, split, storageKey }: Props) {
|
|||||||
document.body.classList.remove("no-select-global");
|
document.body.classList.remove("no-select-global");
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
|
onResize?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousemove", onMouseMove);
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
@@ -255,6 +335,9 @@ function Splitter({ children, direction, split, storageKey }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [splitPosition, setSplitPosition] = useState<number>(50);
|
const [splitPosition, setSplitPosition] = useState<number>(50);
|
||||||
|
const [isClosed, setIsClosed] = useState(false);
|
||||||
|
const [closedSplitPosition, setClosedSplitPosition] =
|
||||||
|
useState<number>(splitPosition);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
useMountEffect(() => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"splitter flex h-full w-full overflow-hidden",
|
"splitter flex h-full w-full overflow-hidden",
|
||||||
direction === "vertical" ? "flex-row" : "flex-col",
|
direction === "vertical" ? "flex-row" : "flex-col",
|
||||||
|
className || "",
|
||||||
)}
|
)}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{direction === "vertical" ? (
|
{direction === "vertical" ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn("left h-full", {
|
className={cn(
|
||||||
"pointer-events-none cursor-col-resize select-none opacity-80":
|
"left h-full",
|
||||||
isDragging,
|
{
|
||||||
})}
|
"pointer-events-none cursor-col-resize select-none opacity-80":
|
||||||
|
isDragging,
|
||||||
|
"overflow-x-hidden": direction === "vertical",
|
||||||
|
"overflow-y-hidden": direction !== "vertical",
|
||||||
|
},
|
||||||
|
classNameLeft,
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `calc(${splitPosition}% - (${splitterThickness} / 2))`,
|
width: `calc(${splitPosition}% - (${splitterThickness} / 2))`,
|
||||||
}}
|
}}
|
||||||
@@ -310,16 +423,34 @@ function Splitter({ children, direction, split, storageKey }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"splitter-bar z-[0] h-full w-[5px] cursor-col-resize bg-[#ccc] opacity-10 hover:opacity-90",
|
"splitter-bar relative z-[0] flex h-full w-[10px] cursor-col-resize items-center justify-center opacity-50 hover:opacity-100",
|
||||||
{ "opacity-90": isDragging },
|
{ "opacity-90": isDragging },
|
||||||
)}
|
)}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
></div>
|
onDoubleClick={snap}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn("h-full w-[2px] bg-slate-800", {
|
||||||
|
"bg-slate-700": isDragging,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
direction={direction}
|
||||||
|
isDragging={isDragging}
|
||||||
|
onDoubleClick={snap}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn("right h-full", {
|
className={cn(
|
||||||
"pointer-events-none cursor-col-resize select-none opacity-80":
|
"right h-full",
|
||||||
isDragging,
|
{
|
||||||
})}
|
"pointer-events-none cursor-col-resize select-none opacity-80":
|
||||||
|
isDragging,
|
||||||
|
"overflow-x-hidden": direction === "vertical",
|
||||||
|
"overflow-y-hidden": direction !== "vertical",
|
||||||
|
},
|
||||||
|
classNameRight,
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `calc(100% - ${splitPosition}% - (${splitterThickness} / 2))`,
|
width: `calc(100% - ${splitPosition}% - (${splitterThickness} / 2))`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ function convertToParametersYAML(
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hideBackground?: boolean;
|
hideBackground?: boolean;
|
||||||
|
hideControls?: boolean;
|
||||||
nodes: Array<AppNode>;
|
nodes: Array<AppNode>;
|
||||||
edges: Array<Edge>;
|
edges: Array<Edge>;
|
||||||
setNodes: (nodes: Array<AppNode>) => void;
|
setNodes: (nodes: Array<AppNode>) => void;
|
||||||
@@ -233,10 +234,12 @@ type Props = {
|
|||||||
onDebuggableBlockCountChange?: (count: number) => void;
|
onDebuggableBlockCountChange?: (count: number) => void;
|
||||||
onMouseDownCapture?: () => void;
|
onMouseDownCapture?: () => void;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
|
onContainerResize?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FlowRenderer({
|
function FlowRenderer({
|
||||||
hideBackground = false,
|
hideBackground = false,
|
||||||
|
hideControls = false,
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
setNodes,
|
setNodes,
|
||||||
@@ -249,6 +252,7 @@ function FlowRenderer({
|
|||||||
onDebuggableBlockCountChange,
|
onDebuggableBlockCountChange,
|
||||||
onMouseDownCapture,
|
onMouseDownCapture,
|
||||||
zIndex,
|
zIndex,
|
||||||
|
onContainerResize,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
@@ -485,14 +489,34 @@ function FlowRenderer({
|
|||||||
|
|
||||||
useAutoPan(editorElementRef, nodes);
|
useAutoPan(editorElementRef, nodes);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
doLayout(nodes, edges);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [onContainerResize]);
|
||||||
|
|
||||||
const zoomLock = 1 as const;
|
const zoomLock = 1 as const;
|
||||||
const yLockMax = 140 as const;
|
const yLockMax = 140 as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO(jdo): hack
|
* 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 = () => {
|
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) => {
|
useOnChange(debugStore.isDebugMode, (newValue) => {
|
||||||
@@ -673,7 +697,7 @@ function FlowRenderer({
|
|||||||
{!hideBackground && (
|
{!hideBackground && (
|
||||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||||
)}
|
)}
|
||||||
<Controls position="bottom-left" />
|
{!hideControls && <Controls position="bottom-left" />}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</BlockActionContext.Provider>
|
</BlockActionContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
PowerButton,
|
PowerButton,
|
||||||
ReloadButton,
|
ReloadButton,
|
||||||
} from "@/components/FloatingWindow";
|
} from "@/components/FloatingWindow";
|
||||||
|
import { Splitter } from "@/components/Splitter";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -127,6 +128,7 @@ function Workspace({
|
|||||||
const [showPowerButton, setShowPowerButton] = useState(true);
|
const [showPowerButton, setShowPowerButton] = useState(true);
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const [windowResizeTrigger, setWindowResizeTrigger] = useState(0);
|
const [windowResizeTrigger, setWindowResizeTrigger] = useState(0);
|
||||||
|
const [containerResizeTrigger, setContainerResizeTrigger] = useState(0);
|
||||||
const [isReloading, setIsReloading] = useState(false);
|
const [isReloading, setIsReloading] = useState(false);
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -206,10 +208,10 @@ function Workspace({
|
|||||||
const hasLoopBlock = nodes.some((node) => node.type === "loop");
|
const hasLoopBlock = nodes.some((node) => node.type === "loop");
|
||||||
const hasHttpBlock = nodes.some((node) => node.type === "http_request");
|
const hasHttpBlock = nodes.some((node) => node.type === "http_request");
|
||||||
const workflowWidth = hasHttpBlock
|
const workflowWidth = hasHttpBlock
|
||||||
? "39rem"
|
? "35.1rem"
|
||||||
: hasLoopBlock
|
: hasLoopBlock
|
||||||
? "34.25rem"
|
? "31.25rem"
|
||||||
: "33rem";
|
: "30rem";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new tab (not window) with the browser session URL.
|
* 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 */}
|
{/* infinite canvas, sub panels, browser, and timeline when in debug mode */}
|
||||||
{showBrowser && (
|
{showBrowser && (
|
||||||
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
|
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
|
||||||
{/* infinite canvas */}
|
<Splitter
|
||||||
<div
|
className="splittah"
|
||||||
className="skyvern-split-left h-full"
|
classNameLeft="flex items-center justify-center"
|
||||||
style={{
|
direction="vertical"
|
||||||
width: workflowWidth,
|
split={{ left: workflowWidth }}
|
||||||
maxWidth: workflowWidth,
|
onResize={() => setContainerResizeTrigger((prev) => prev + 1)}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FlowRenderer
|
{/* infinite canvas */}
|
||||||
hideBackground={true}
|
<div className="skyvern-split-left h-full w-full">
|
||||||
nodes={nodes}
|
<FlowRenderer
|
||||||
edges={edges}
|
hideBackground={true}
|
||||||
setNodes={setNodes}
|
hideControls={true}
|
||||||
setEdges={setEdges}
|
nodes={nodes}
|
||||||
onNodesChange={onNodesChange}
|
edges={edges}
|
||||||
onEdgesChange={onEdgesChange}
|
setNodes={setNodes}
|
||||||
initialTitle={initialTitle}
|
setEdges={setEdges}
|
||||||
workflow={workflow}
|
onNodesChange={onNodesChange}
|
||||||
/>
|
onEdgesChange={onEdgesChange}
|
||||||
</div>
|
initialTitle={initialTitle}
|
||||||
|
workflow={workflow}
|
||||||
|
onContainerResize={containerResizeTrigger}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* divider*/}
|
{/* browser & timeline & sub-panels in debug mode */}
|
||||||
<div className="mt-[8rem] h-[calc(100%_-_8rem)] w-[1px] bg-slate-800" />
|
<div className="skyvern-split-right relative flex h-full items-end justify-center bg-[#020617] p-4 pl-6">
|
||||||
|
{/* sub panels */}
|
||||||
{/* browser & timeline & sub-panels in debug mode */}
|
{workflowPanelState.active && (
|
||||||
<div
|
|
||||||
className="skyvern-split-right relative flex h-full items-end justify-center bg-[#020617] p-4 pl-6"
|
|
||||||
style={{
|
|
||||||
width: `calc(100% - ${workflowWidth})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* sub panels */}
|
|
||||||
{workflowPanelState.active && (
|
|
||||||
<div
|
|
||||||
className={cn("absolute right-6 top-[8.5rem] z-30", {
|
|
||||||
"left-6": workflowPanelState.content === "nodeLibrary",
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
height:
|
|
||||||
workflowPanelState.content === "nodeLibrary"
|
|
||||||
? "calc(100vh - 14rem)"
|
|
||||||
: "unset",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{workflowPanelState.content === "cacheKeyValues" && (
|
|
||||||
<WorkflowCacheKeyValuesPanel
|
|
||||||
cacheKeyValues={cacheKeyValues}
|
|
||||||
pending={cacheKeyValuesLoading}
|
|
||||||
scriptKey={workflow.cache_key ?? "default"}
|
|
||||||
onDelete={(cacheKeyValue) => {
|
|
||||||
setToDeleteCacheKeyValue(cacheKeyValue);
|
|
||||||
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
|
||||||
}}
|
|
||||||
onPaginate={(page) => {
|
|
||||||
setPage(page);
|
|
||||||
}}
|
|
||||||
onSelect={(cacheKeyValue) => {
|
|
||||||
setCacheKeyValue(cacheKeyValue);
|
|
||||||
setCacheKeyValueFilter("");
|
|
||||||
closeWorkflowPanel();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{workflowPanelState.content === "parameters" && (
|
|
||||||
<WorkflowParametersPanel />
|
|
||||||
)}
|
|
||||||
{workflowPanelState.content === "nodeLibrary" && (
|
|
||||||
<WorkflowNodeLibraryPanel
|
|
||||||
onNodeClick={(props) => {
|
|
||||||
addNode(props);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* browser & timeline */}
|
|
||||||
<div className="flex h-[calc(100%_-_8rem)] w-full gap-6">
|
|
||||||
{/* browser */}
|
|
||||||
<div className="flex h-full w-[calc(100%_-_6rem)] flex-1 flex-col items-center justify-center">
|
|
||||||
<div key={reloadKey} className="w-full flex-1">
|
|
||||||
{activeDebugSession &&
|
|
||||||
activeDebugSession.browser_session_id &&
|
|
||||||
!cycleBrowser.isPending ? (
|
|
||||||
<BrowserStream
|
|
||||||
interactive={interactor === "human"}
|
|
||||||
browserSessionId={activeDebugSession.browser_session_id}
|
|
||||||
showControlButtons={interactor === "human"}
|
|
||||||
resizeTrigger={windowResizeTrigger}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-md border border-slate-800 pb-2 pt-4 text-sm text-slate-400">
|
|
||||||
Connecting to your browser...
|
|
||||||
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<footer className="flex h-[2rem] w-full items-center justify-start gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<GlobeIcon /> Live Browser
|
|
||||||
</div>
|
|
||||||
{showBreakoutButton && (
|
|
||||||
<BreakoutButton onClick={() => breakout()} />
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn("ml-auto flex items-center gap-2", {
|
|
||||||
"mr-16": !blockLabel,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{showPowerButton && <PowerButton onClick={() => cycle()} />}
|
|
||||||
<ReloadButton
|
|
||||||
isReloading={isReloading}
|
|
||||||
onClick={() => reload()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* timeline */}
|
|
||||||
<div
|
|
||||||
className={cn("z-20 h-full w-[5rem] overflow-visible", {
|
|
||||||
"pointer-events-none hidden w-[0px] overflow-hidden":
|
|
||||||
!blockLabel,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("absolute right-6 top-[8.5rem] z-30", {
|
||||||
"relative h-full w-[25rem] translate-x-[-20.5rem] bg-[#020617] transition-all",
|
"left-6": workflowPanelState.content === "nodeLibrary",
|
||||||
{
|
})}
|
||||||
"translate-x-[0rem]": timelineMode === "narrow",
|
style={{
|
||||||
group: timelineMode === "narrow",
|
height:
|
||||||
},
|
workflowPanelState.content === "nodeLibrary"
|
||||||
)}
|
? "calc(100vh - 14rem)"
|
||||||
onClick={() => {
|
: "unset",
|
||||||
if (timelineMode === "narrow") {
|
|
||||||
setTimelineMode("wide");
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* timeline wide */}
|
{workflowPanelState.content === "cacheKeyValues" && (
|
||||||
<div
|
<WorkflowCacheKeyValuesPanel
|
||||||
className={cn(
|
cacheKeyValues={cacheKeyValues}
|
||||||
"pointer-events-none absolute left-[0.5rem] right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end opacity-0 transition-all duration-1000",
|
pending={cacheKeyValuesLoading}
|
||||||
{ "opacity-100": timelineMode === "wide" },
|
scriptKey={workflow.cache_key ?? "default"}
|
||||||
|
onDelete={(cacheKeyValue) => {
|
||||||
|
setToDeleteCacheKeyValue(cacheKeyValue);
|
||||||
|
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
||||||
|
}}
|
||||||
|
onPaginate={(page) => {
|
||||||
|
setPage(page);
|
||||||
|
}}
|
||||||
|
onSelect={(cacheKeyValue) => {
|
||||||
|
setCacheKeyValue(cacheKeyValue);
|
||||||
|
setCacheKeyValueFilter("");
|
||||||
|
closeWorkflowPanel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{workflowPanelState.content === "parameters" && (
|
||||||
|
<WorkflowParametersPanel />
|
||||||
|
)}
|
||||||
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* browser & timeline */}
|
||||||
|
<div className="flex h-[calc(100%_-_8rem)] w-full gap-6">
|
||||||
|
{/* browser */}
|
||||||
|
<div className="flex h-full w-[calc(100%_-_6rem)] flex-1 flex-col items-center justify-center">
|
||||||
|
<div key={reloadKey} className="w-full flex-1">
|
||||||
|
{activeDebugSession &&
|
||||||
|
activeDebugSession.browser_session_id &&
|
||||||
|
!cycleBrowser.isPending ? (
|
||||||
|
<BrowserStream
|
||||||
|
interactive={interactor === "human"}
|
||||||
|
browserSessionId={activeDebugSession.browser_session_id}
|
||||||
|
showControlButtons={interactor === "human"}
|
||||||
|
resizeTrigger={windowResizeTrigger}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-md border border-slate-800 pb-2 pt-4 text-sm text-slate-400">
|
||||||
|
Connecting to your browser...
|
||||||
|
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
<div
|
<footer className="flex h-[2rem] w-full items-center justify-start gap-4">
|
||||||
className={cn(
|
<div className="flex items-center gap-2">
|
||||||
"pointer-events-none relative flex h-full w-full flex-col items-start overflow-hidden bg-[#020617]",
|
<GlobeIcon /> Live Browser
|
||||||
{ "pointer-events-auto": timelineMode === "wide" },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DebuggerRun />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{showBreakoutButton && (
|
||||||
|
<BreakoutButton onClick={() => breakout()} />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn("ml-auto flex items-center gap-2", {
|
||||||
|
"mr-16": !blockLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{showPowerButton && (
|
||||||
|
<PowerButton onClick={() => cycle()} />
|
||||||
|
)}
|
||||||
|
<ReloadButton
|
||||||
|
isReloading={isReloading}
|
||||||
|
onClick={() => reload()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* divider */}
|
{/* timeline */}
|
||||||
<div className="vertical-line-gradient absolute left-0 top-0 h-full w-[2px]"></div>
|
<div
|
||||||
|
className={cn("z-20 h-full w-[5rem] overflow-visible", {
|
||||||
{/* slide indicator */}
|
"pointer-events-none hidden w-[0px] overflow-hidden":
|
||||||
<div
|
!blockLabel,
|
||||||
className="absolute left-0 top-0 z-20 flex h-full items-center justify-center p-1 opacity-30 transition-opacity hover:opacity-100 group-hover:opacity-100"
|
})}
|
||||||
onClick={(e) => {
|
>
|
||||||
e.stopPropagation();
|
|
||||||
setTimelineMode(
|
|
||||||
timelineMode === "wide" ? "narrow" : "wide",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{timelineMode === "narrow" && <ChevronLeftIcon />}
|
|
||||||
{timelineMode === "wide" && <ChevronRightIcon />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* timeline narrow */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"delay-[300ms] pointer-events-none absolute left-0 top-0 h-full w-[6rem] rounded-l-lg opacity-0 transition-all duration-1000",
|
"relative h-full w-[25rem] translate-x-[-20.5rem] bg-[#020617] transition-all",
|
||||||
{
|
{
|
||||||
"pointer-events-auto opacity-100":
|
"translate-x-[0rem]": timelineMode === "narrow",
|
||||||
timelineMode === "narrow",
|
group: timelineMode === "narrow",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (timelineMode === "narrow") {
|
||||||
|
setTimelineMode("wide");
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DebuggerRunMinimal />
|
{/* timeline wide */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute left-[0.5rem] right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end opacity-0 transition-all duration-1000",
|
||||||
|
{ "opacity-100": timelineMode === "wide" },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none relative flex h-full w-full flex-col items-start overflow-hidden bg-[#020617]",
|
||||||
|
{ "pointer-events-auto": timelineMode === "wide" },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DebuggerRun />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* divider */}
|
||||||
|
<div className="vertical-line-gradient absolute left-0 top-0 h-full w-[2px]"></div>
|
||||||
|
|
||||||
|
{/* slide indicator */}
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 z-20 flex h-full items-center justify-center p-1 opacity-30 transition-opacity hover:opacity-100 group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setTimelineMode(
|
||||||
|
timelineMode === "wide" ? "narrow" : "wide",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timelineMode === "narrow" && <ChevronLeftIcon />}
|
||||||
|
{timelineMode === "wide" && <ChevronRightIcon />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* timeline narrow */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"delay-[300ms] pointer-events-none absolute left-0 top-0 h-full w-[6rem] rounded-l-lg opacity-0 transition-all duration-1000",
|
||||||
|
{
|
||||||
|
"pointer-events-auto opacity-100":
|
||||||
|
timelineMode === "narrow",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DebuggerRunMinimal />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Splitter>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,3 +21,8 @@
|
|||||||
.react-flow__handle-bottom {
|
.react-flow__handle-bottom {
|
||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__attribution {
|
||||||
|
background-color: rgb(2 6 23);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user