multiple UI fixes/updates (#3422)
This commit is contained in:
@@ -343,9 +343,12 @@ function BrowserStream({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("browser-stream flex items-center justify-center", {
|
className={cn(
|
||||||
"user-is-controlling": theUserIsControlling,
|
"browser-stream relative flex items-center justify-center",
|
||||||
})}
|
{
|
||||||
|
"user-is-controlling": theUserIsControlling,
|
||||||
|
},
|
||||||
|
)}
|
||||||
ref={setCanvasContainerRef}
|
ref={setCanvasContainerRef}
|
||||||
>
|
>
|
||||||
{isVncConnected && (
|
{isVncConnected && (
|
||||||
@@ -384,7 +387,7 @@ function BrowserStream({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isVncConnected && (
|
{!isVncConnected && (
|
||||||
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-md border border-slate-800 text-sm text-slate-400">
|
<div className="absolute left-0 top-1/2 flex aspect-video w-full -translate-y-1/2 flex-col items-center justify-center gap-2 rounded-md border border-slate-800 text-sm text-slate-400">
|
||||||
<RotateThrough interval={7 * 1000}>
|
<RotateThrough interval={7 * 1000}>
|
||||||
<span>Hm, working on the connection...</span>
|
<span>Hm, working on the connection...</span>
|
||||||
<span>Hang tight, we're almost there...</span>
|
<span>Hang tight, we're almost there...</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useRef, useState, RefObject } from "react";
|
import { useRef, useState, RefObject } from "react";
|
||||||
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";
|
||||||
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
|
|
||||||
function Handle({
|
function Handle({
|
||||||
direction,
|
direction,
|
||||||
@@ -341,21 +341,30 @@ function Splitter({
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
if (containerRef.current) {
|
// small delay here, to allow for arbitrary layout thrashing to settle;
|
||||||
const newPosition = normalizeUnitsToPercent(
|
// otherwise we have to rely on an observer for the container size, and
|
||||||
containerRef,
|
// resetting whenever the container resizes it likely incorrect behaviour
|
||||||
direction,
|
setTimeout(() => {
|
||||||
firstSizingTarget,
|
if (containerRef.current) {
|
||||||
firstSizing,
|
const newPosition = normalizeUnitsToPercent(
|
||||||
storageKey,
|
containerRef,
|
||||||
);
|
direction,
|
||||||
|
firstSizingTarget,
|
||||||
|
firstSizing,
|
||||||
|
storageKey,
|
||||||
|
);
|
||||||
|
|
||||||
setSplitPosition(newPosition);
|
setSplitPosition(newPosition);
|
||||||
|
|
||||||
if (storageKey) {
|
if (storageKey) {
|
||||||
setStoredSizing(firstSizingTarget, storageKey, newPosition.toString());
|
setStoredSizing(
|
||||||
|
firstSizingTarget,
|
||||||
|
storageKey,
|
||||||
|
newPosition.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
useOnChange(isDragging, (newValue, oldValue) => {
|
useOnChange(isDragging, (newValue, oldValue) => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { TaskDetails } from "./routes/tasks/detail/TaskDetails";
|
|||||||
import { TaskParameters } from "./routes/tasks/detail/TaskParameters";
|
import { TaskParameters } from "./routes/tasks/detail/TaskParameters";
|
||||||
import { TaskRecording } from "./routes/tasks/detail/TaskRecording";
|
import { TaskRecording } from "./routes/tasks/detail/TaskRecording";
|
||||||
import { TasksPage } from "./routes/tasks/list/TasksPage";
|
import { TasksPage } from "./routes/tasks/list/TasksPage";
|
||||||
|
import { Debugger } from "@/routes/workflows/debugger/Debugger";
|
||||||
import { WorkflowPage } from "./routes/workflows/WorkflowPage";
|
import { WorkflowPage } from "./routes/workflows/WorkflowPage";
|
||||||
import { WorkflowRun } from "./routes/workflows/WorkflowRun";
|
import { WorkflowRun } from "./routes/workflows/WorkflowRun";
|
||||||
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";
|
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";
|
||||||
@@ -110,11 +111,11 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "debug",
|
path: "debug",
|
||||||
element: <WorkflowEditor />,
|
element: <Debugger />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":workflowRunId/:blockLabel/debug",
|
path: ":workflowRunId/:blockLabel/debug",
|
||||||
element: <WorkflowEditor />,
|
element: <Debugger />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useEffect, useRef, useState, useMemo } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -17,6 +17,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
||||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||||
|
import { WorkflowRunStream } from "@/routes/workflows/workflowRun/WorkflowRunStream";
|
||||||
import { useCacheKeyValuesQuery } from "../hooks/useCacheKeyValuesQuery";
|
import { useCacheKeyValuesQuery } from "../hooks/useCacheKeyValuesQuery";
|
||||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
import { useSidebarStore } from "@/store/SidebarStore";
|
||||||
@@ -137,12 +138,6 @@ function Workspace({
|
|||||||
const blockScriptStore = useBlockScriptStore();
|
const blockScriptStore = useBlockScriptStore();
|
||||||
const cacheKey = workflow?.cache_key ?? "";
|
const cacheKey = workflow?.cache_key ?? "";
|
||||||
|
|
||||||
const enableDebugBrowser = useMemo(() => {
|
|
||||||
return (
|
|
||||||
showBrowser && (activeDebugSession?.vnc_streaming_supported ?? false)
|
|
||||||
);
|
|
||||||
}, [showBrowser, activeDebugSession?.vnc_streaming_supported]);
|
|
||||||
|
|
||||||
const [cacheKeyValue, setCacheKeyValue] = useState(
|
const [cacheKeyValue, setCacheKeyValue] = useState(
|
||||||
cacheKey === ""
|
cacheKey === ""
|
||||||
? ""
|
? ""
|
||||||
@@ -215,10 +210,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
|
||||||
? "35.1rem"
|
? "39rem"
|
||||||
: hasLoopBlock
|
: hasLoopBlock
|
||||||
? "31.25rem"
|
? "34.25rem"
|
||||||
: "30rem";
|
: "34rem";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new tab (not window) with the browser session URL.
|
* Open a new tab (not window) with the browser session URL.
|
||||||
@@ -705,7 +700,7 @@ function Workspace({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* infinite canvas and sub panels when not in debug mode */}
|
{/* infinite canvas and sub panels when not in debug mode */}
|
||||||
{!enableDebugBrowser && (
|
{!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 */}
|
{/* infinite canvas */}
|
||||||
<FlowRenderer
|
<FlowRenderer
|
||||||
@@ -722,7 +717,7 @@ function Workspace({
|
|||||||
{/* sub panels */}
|
{/* sub panels */}
|
||||||
{workflowPanelState.active && (
|
{workflowPanelState.active && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute right-6 top-[5rem] z-30"
|
className="absolute right-6 top-[8.5rem] z-30"
|
||||||
style={{
|
style={{
|
||||||
height:
|
height:
|
||||||
workflowPanelState.content === "nodeLibrary"
|
workflowPanelState.content === "nodeLibrary"
|
||||||
@@ -750,12 +745,12 @@ function Workspace({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{workflowPanelState.content === "parameters" && (
|
{workflowPanelState.content === "parameters" && (
|
||||||
<div className="pointer-events-auto relative right-0 top-[3.5rem] z-30">
|
<div className="z-30">
|
||||||
<WorkflowParametersPanel />
|
<WorkflowParametersPanel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{workflowPanelState.content === "nodeLibrary" && (
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
<div className="pointer-events-auto relative right-0 top-[3.5rem] z-30 h-full w-[25rem]">
|
<div className="z-30 h-full w-[25rem]">
|
||||||
<WorkflowNodeLibraryPanel
|
<WorkflowNodeLibraryPanel
|
||||||
onNodeClick={(props) => {
|
onNodeClick={(props) => {
|
||||||
addNode(props);
|
addNode(props);
|
||||||
@@ -768,8 +763,51 @@ function Workspace({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* infinite canvas, sub panels, browser, and timeline when in debug mode */}
|
{/* sub panels when in debug mode */}
|
||||||
{enableDebugBrowser && (
|
{showBrowser && workflowPanelState.active && (
|
||||||
|
<div
|
||||||
|
className="absolute right-6 top-[8.5rem] z-30"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* infinite canvas, browser, and timeline when in debug mode */}
|
||||||
|
{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">
|
||||||
<Splitter
|
<Splitter
|
||||||
className="splittah"
|
className="splittah"
|
||||||
@@ -795,96 +833,65 @@ function Workspace({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* browser & timeline & sub-panels in debug mode */}
|
{/* browser & timeline */}
|
||||||
<div className="skyvern-split-right relative flex h-full items-end justify-center bg-[#020617] p-4 pl-6">
|
<div className="skyvern-split-right relative flex h-full items-end justify-center bg-[#020617] p-4 pl-6">
|
||||||
{/* 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">
|
<div className="flex h-[calc(100%_-_8rem)] w-full gap-6">
|
||||||
{/* browser */}
|
{/* VNC browser */}
|
||||||
<div className="flex h-full w-[calc(100%_-_6rem)] flex-1 flex-col items-center justify-center">
|
{!activeDebugSession ||
|
||||||
<div key={reloadKey} className="w-full flex-1">
|
(activeDebugSession.vnc_streaming_supported && (
|
||||||
{activeDebugSession &&
|
<div className="skyvern-vnc-browser flex h-full w-[calc(100%_-_6rem)] flex-1 flex-col items-center justify-center">
|
||||||
activeDebugSession.browser_session_id &&
|
<div key={reloadKey} className="w-full flex-1">
|
||||||
!cycleBrowser.isPending ? (
|
{activeDebugSession &&
|
||||||
<BrowserStream
|
activeDebugSession.browser_session_id &&
|
||||||
interactive={interactor === "human"}
|
!cycleBrowser.isPending ? (
|
||||||
browserSessionId={activeDebugSession.browser_session_id}
|
<BrowserStream
|
||||||
showControlButtons={interactor === "human"}
|
interactive={interactor === "human"}
|
||||||
resizeTrigger={windowResizeTrigger}
|
browserSessionId={
|
||||||
/>
|
activeDebugSession.browser_session_id
|
||||||
) : (
|
}
|
||||||
<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">
|
showControlButtons={interactor === "human"}
|
||||||
Connecting to your browser...
|
resizeTrigger={windowResizeTrigger}
|
||||||
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
/>
|
||||||
|
) : (
|
||||||
|
<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">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<footer className="flex h-[2rem] w-full items-center justify-start gap-4">
|
<GlobeIcon /> Live Browser
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
<GlobeIcon /> Live Browser
|
{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>
|
</div>
|
||||||
{showBreakoutButton && (
|
))}
|
||||||
<BreakoutButton onClick={() => breakout()} />
|
|
||||||
)}
|
{/* Screenshot browser} */}
|
||||||
<div
|
{activeDebugSession &&
|
||||||
className={cn("ml-auto flex items-center gap-2", {
|
!activeDebugSession.vnc_streaming_supported && (
|
||||||
"mr-16": !blockLabel,
|
<div className="skyvern-screenshot-browser flex h-full w-[calc(100%_-_6rem)] flex-1 flex-col items-center justify-center">
|
||||||
})}
|
<div className="aspect-video w-full">
|
||||||
>
|
<WorkflowRunStream alwaysShowStream={true} />
|
||||||
{showPowerButton && (
|
</div>
|
||||||
<PowerButton onClick={() => cycle()} />
|
|
||||||
)}
|
|
||||||
<ReloadButton
|
|
||||||
isReloading={isReloading}
|
|
||||||
onClick={() => reload()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* timeline */}
|
{/* timeline */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,14 +15,20 @@ type StreamMessage = {
|
|||||||
screenshot?: string;
|
screenshot?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
alwaysShowStream?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
|
|
||||||
const wssBaseUrl = import.meta.env.VITE_WSS_BASE_URL;
|
const wssBaseUrl = import.meta.env.VITE_WSS_BASE_URL;
|
||||||
|
|
||||||
function WorkflowRunStream() {
|
function WorkflowRunStream(props?: Props) {
|
||||||
|
const alwaysShowStream = props?.alwaysShowStream ?? false;
|
||||||
const { data: workflowRun } = useWorkflowRunQuery();
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
const [streamImgSrc, setStreamImgSrc] = useState<string>("");
|
const [streamImgSrc, setStreamImgSrc] = useState<string>("");
|
||||||
const showStream = workflowRun && statusIsNotFinalized(workflowRun);
|
const showStream =
|
||||||
|
alwaysShowStream || (workflowRun && statusIsNotFinalized(workflowRun));
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const { workflowRunId, workflowPermanentId } = useParams();
|
const { workflowRunId, workflowPermanentId } = useParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -149,6 +155,26 @@ function WorkflowRunStream() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (alwaysShowStream) {
|
||||||
|
if (streamImgSrc?.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<ZoomableImage
|
||||||
|
src={`data:image/png;base64,${streamImgSrc}`}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
Waiting for stream...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user