Jon/workspace (#3175)
This commit is contained in:
@@ -158,6 +158,8 @@ function FloatingWindow({
|
|||||||
zIndex,
|
zIndex,
|
||||||
// --
|
// --
|
||||||
onCycle,
|
onCycle,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
onInteract,
|
onInteract,
|
||||||
}: {
|
}: {
|
||||||
bounded?: boolean;
|
bounded?: boolean;
|
||||||
@@ -172,9 +174,11 @@ function FloatingWindow({
|
|||||||
showPowerButton?: boolean;
|
showPowerButton?: boolean;
|
||||||
showReloadButton?: boolean;
|
showReloadButton?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
zIndex?: string;
|
zIndex?: number;
|
||||||
// --
|
// --
|
||||||
onCycle?: () => void;
|
onCycle?: () => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
onInteract?: () => void;
|
onInteract?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
@@ -217,6 +221,7 @@ function FloatingWindow({
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
const os = getOs();
|
const os = getOs();
|
||||||
|
|
||||||
@@ -284,9 +289,10 @@ function FloatingWindow({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialWidth || !initialHeight) {
|
if (hasInitialized.current || !initialWidth || !initialHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
hasInitialized.current = true;
|
||||||
setSize({
|
setSize({
|
||||||
left: initialPosition?.x ?? 0,
|
left: initialPosition?.x ?? 0,
|
||||||
top: initialPosition?.y ?? 0,
|
top: initialPosition?.y ?? 0,
|
||||||
@@ -533,6 +539,16 @@ function FloatingWindow({
|
|||||||
className={cn("border-2 border-gray-700", {
|
className={cn("border-2 border-gray-700", {
|
||||||
"hover:border-slate-500": !isMaximized,
|
"hover:border-slate-500": !isMaximized,
|
||||||
})}
|
})}
|
||||||
|
handleStyles={{
|
||||||
|
bottomLeft: {
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
},
|
||||||
|
bottomRight: {
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
minHeight={Constants.MinHeight}
|
minHeight={Constants.MinHeight}
|
||||||
minWidth={Constants.MinWidth}
|
minWidth={Constants.MinWidth}
|
||||||
// TODO: turn back on; turning off clears a resize bug atm
|
// TODO: turn back on; turning off clears a resize bug atm
|
||||||
@@ -556,6 +572,7 @@ function FloatingWindow({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFocus?.();
|
||||||
setIsMinimized(false);
|
setIsMinimized(false);
|
||||||
setIsResizing(true);
|
setIsResizing(true);
|
||||||
setDragStartSize({ ...size, left: position.x, top: position.y });
|
setDragStartSize({ ...size, left: position.x, top: position.y });
|
||||||
@@ -565,6 +582,7 @@ function FloatingWindow({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFocus?.();
|
||||||
onResize({ delta, direction, size });
|
onResize({ delta, direction, size });
|
||||||
}}
|
}}
|
||||||
onResizeStop={() => {
|
onResizeStop={() => {
|
||||||
@@ -581,7 +599,8 @@ function FloatingWindow({
|
|||||||
<div
|
<div
|
||||||
ref={resizableRef}
|
ref={resizableRef}
|
||||||
key={reloadKey}
|
key={reloadKey}
|
||||||
className="my-window"
|
className="my-window focus:outline-none"
|
||||||
|
tabIndex={-1}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
padding: "0px",
|
padding: "0px",
|
||||||
@@ -590,7 +609,12 @@ function FloatingWindow({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
onMouseDownCapture={() => onInteract?.()}
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onMouseDownCapture={(e) => {
|
||||||
|
onInteract?.();
|
||||||
|
e.currentTarget.focus();
|
||||||
|
}}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
toggleMaximized();
|
toggleMaximized();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
|
||||||
import { useOnChange } from "@/hooks/useOnChange";
|
import { useOnChange } from "@/hooks/useOnChange";
|
||||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||||
import { BlockActionContext } from "@/store/BlockActionContext";
|
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||||
@@ -17,7 +16,6 @@ import {
|
|||||||
useWorkflowSave,
|
useWorkflowSave,
|
||||||
type WorkflowSaveData,
|
type WorkflowSaveData,
|
||||||
} from "@/store/WorkflowHasChangesStore";
|
} from "@/store/WorkflowHasChangesStore";
|
||||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
|
||||||
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
@@ -25,17 +23,15 @@ import {
|
|||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
Controls,
|
Controls,
|
||||||
Edge,
|
Edge,
|
||||||
Panel,
|
|
||||||
PanOnScrollMode,
|
PanOnScrollMode,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Viewport,
|
Viewport,
|
||||||
useEdgesState,
|
|
||||||
useNodesInitialized,
|
useNodesInitialized,
|
||||||
useNodesState,
|
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
|
NodeChange,
|
||||||
|
EdgeChange,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useBlocker } from "react-router-dom";
|
import { useBlocker } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
@@ -56,22 +52,13 @@ import {
|
|||||||
ParameterYAML,
|
ParameterYAML,
|
||||||
WorkflowParameterYAML,
|
WorkflowParameterYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
|
||||||
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
|
||||||
import {
|
import {
|
||||||
BITWARDEN_CLIENT_ID_AWS_SECRET_KEY,
|
BITWARDEN_CLIENT_ID_AWS_SECRET_KEY,
|
||||||
BITWARDEN_CLIENT_SECRET_AWS_SECRET_KEY,
|
BITWARDEN_CLIENT_SECRET_AWS_SECRET_KEY,
|
||||||
BITWARDEN_MASTER_PASSWORD_AWS_SECRET_KEY,
|
BITWARDEN_MASTER_PASSWORD_AWS_SECRET_KEY,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { edgeTypes } from "./edges";
|
import { edgeTypes } from "./edges";
|
||||||
import {
|
import { AppNode, isWorkflowBlockNode, nodeTypes } from "./nodes";
|
||||||
AppNode,
|
|
||||||
isWorkflowBlockNode,
|
|
||||||
nodeTypes,
|
|
||||||
WorkflowBlockNode,
|
|
||||||
} from "./nodes";
|
|
||||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
|
||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
|
||||||
import {
|
import {
|
||||||
ParametersState,
|
ParametersState,
|
||||||
parameterIsSkyvernCredential,
|
parameterIsSkyvernCredential,
|
||||||
@@ -81,22 +68,14 @@ import {
|
|||||||
import "./reactFlowOverrideStyles.css";
|
import "./reactFlowOverrideStyles.css";
|
||||||
import {
|
import {
|
||||||
convertEchoParameters,
|
convertEchoParameters,
|
||||||
createNode,
|
|
||||||
defaultEdge,
|
|
||||||
descendants,
|
descendants,
|
||||||
generateNodeLabel,
|
|
||||||
getAdditionalParametersForEmailBlock,
|
getAdditionalParametersForEmailBlock,
|
||||||
getOrderedChildrenBlocks,
|
getOrderedChildrenBlocks,
|
||||||
getOutputParameterKey,
|
getOutputParameterKey,
|
||||||
getWorkflowBlocks,
|
getWorkflowBlocks,
|
||||||
getWorkflowErrors,
|
|
||||||
getWorkflowSettings,
|
getWorkflowSettings,
|
||||||
layout,
|
layout,
|
||||||
nodeAdderNode,
|
|
||||||
startNode,
|
|
||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
import { cn } from "@/util/utils";
|
|
||||||
import { WorkflowDebuggerRun } from "@/routes/workflows/editor/WorkflowDebuggerRun";
|
|
||||||
import { useAutoPan } from "./useAutoPan";
|
import { useAutoPan } from "./useAutoPan";
|
||||||
|
|
||||||
function convertToParametersYAML(
|
function convertToParametersYAML(
|
||||||
@@ -237,38 +216,38 @@ function convertToParametersYAML(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
nodes: Array<AppNode>;
|
||||||
|
edges: Array<Edge>;
|
||||||
|
setNodes: (nodes: Array<AppNode>) => void;
|
||||||
|
setEdges: (edges: Array<Edge>) => void;
|
||||||
|
onNodesChange: (changes: Array<NodeChange<AppNode>>) => void;
|
||||||
|
onEdgesChange: (changes: Array<EdgeChange>) => void;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
initialNodes: Array<AppNode>;
|
|
||||||
initialEdges: Array<Edge>;
|
|
||||||
initialParameters: ParametersState;
|
initialParameters: ParametersState;
|
||||||
workflow: WorkflowApiResponse;
|
workflow: WorkflowApiResponse;
|
||||||
};
|
onDebuggableBlockCountChange: (count: number) => void;
|
||||||
|
onMouseDownCapture?: () => void;
|
||||||
export type AddNodeProps = {
|
zIndex?: number;
|
||||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
|
||||||
previous: string | null;
|
|
||||||
next: string | null;
|
|
||||||
parent?: string;
|
|
||||||
connectingEdgeType: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function FlowRenderer({
|
function FlowRenderer({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
onNodesChange,
|
||||||
|
onEdgesChange,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
initialEdges,
|
|
||||||
initialNodes,
|
|
||||||
initialParameters,
|
initialParameters,
|
||||||
workflow,
|
workflow,
|
||||||
|
onDebuggableBlockCountChange,
|
||||||
|
onMouseDownCapture,
|
||||||
|
zIndex,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
|
||||||
useWorkflowPanelStore();
|
|
||||||
const { title, initializeTitle } = useWorkflowTitleStore();
|
const { title, initializeTitle } = useWorkflowTitleStore();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [parameters] = useState<ParametersState>(initialParameters);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
||||||
const [parameters, setParameters] =
|
|
||||||
useState<ParametersState>(initialParameters);
|
|
||||||
const [debuggableBlockCount, setDebuggableBlockCount] = useState(0);
|
|
||||||
const nodesInitialized = useNodesInitialized();
|
const nodesInitialized = useNodesInitialized();
|
||||||
const [shouldConstrainPan, setShouldConstrainPan] = useState(false);
|
const [shouldConstrainPan, setShouldConstrainPan] = useState(false);
|
||||||
const onNodesChangeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const onNodesChangeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -324,8 +303,8 @@ function FlowRenderer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDebuggableBlockCount(debuggable.length);
|
onDebuggableBlockCountChange(debuggable.length);
|
||||||
}, [nodes, edges]);
|
}, [nodes, edges, onDebuggableBlockCountChange]);
|
||||||
|
|
||||||
const constructSaveData = useCallback((): WorkflowSaveData => {
|
const constructSaveData = useCallback((): WorkflowSaveData => {
|
||||||
const blocks = getWorkflowBlocks(nodes, edges);
|
const blocks = getWorkflowBlocks(nodes, edges);
|
||||||
@@ -371,88 +350,6 @@ function FlowRenderer({
|
|||||||
return await saveWorkflow.mutateAsync();
|
return await saveWorkflow.mutateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNode({
|
|
||||||
nodeType,
|
|
||||||
previous,
|
|
||||||
next,
|
|
||||||
parent,
|
|
||||||
connectingEdgeType,
|
|
||||||
}: AddNodeProps) {
|
|
||||||
const newNodes: Array<AppNode> = [];
|
|
||||||
const newEdges: Array<Edge> = [];
|
|
||||||
const id = nanoid();
|
|
||||||
const existingLabels = nodes
|
|
||||||
.filter(isWorkflowBlockNode)
|
|
||||||
.map((node) => node.data.label);
|
|
||||||
const node = createNode(
|
|
||||||
{ id, parentId: parent },
|
|
||||||
nodeType,
|
|
||||||
generateNodeLabel(existingLabels),
|
|
||||||
);
|
|
||||||
newNodes.push(node);
|
|
||||||
if (previous) {
|
|
||||||
const newEdge = {
|
|
||||||
id: nanoid(),
|
|
||||||
type: "edgeWithAddButton",
|
|
||||||
source: previous,
|
|
||||||
target: id,
|
|
||||||
style: {
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
newEdges.push(newEdge);
|
|
||||||
}
|
|
||||||
if (next) {
|
|
||||||
const newEdge = {
|
|
||||||
id: nanoid(),
|
|
||||||
type: connectingEdgeType,
|
|
||||||
source: id,
|
|
||||||
target: next,
|
|
||||||
style: {
|
|
||||||
strokeWidth: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
newEdges.push(newEdge);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeType === "loop") {
|
|
||||||
// when loop node is first created it needs an adder node so nodes can be added inside the loop
|
|
||||||
const startNodeId = nanoid();
|
|
||||||
const adderNodeId = nanoid();
|
|
||||||
newNodes.push(
|
|
||||||
startNode(
|
|
||||||
startNodeId,
|
|
||||||
{
|
|
||||||
withWorkflowSettings: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
newNodes.push(nodeAdderNode(adderNodeId, id));
|
|
||||||
newEdges.push(defaultEdge(startNodeId, adderNodeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const editedEdges = previous
|
|
||||||
? edges.filter((edge) => edge.source !== previous)
|
|
||||||
: edges;
|
|
||||||
|
|
||||||
const previousNode = nodes.find((node) => node.id === previous);
|
|
||||||
const previousNodeIndex = previousNode
|
|
||||||
? nodes.indexOf(previousNode)
|
|
||||||
: nodes.length - 1;
|
|
||||||
|
|
||||||
// creating some memory for no reason, maybe check it out later
|
|
||||||
const newNodesAfter = [
|
|
||||||
...nodes.slice(0, previousNodeIndex + 1),
|
|
||||||
...newNodes,
|
|
||||||
...nodes.slice(previousNodeIndex + 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
workflowChangesStore.setHasChanges(true);
|
|
||||||
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteNode(id: string) {
|
function deleteNode(id: string) {
|
||||||
const node = nodes.find((node) => node.id === id);
|
const node = nodes.find((node) => node.id === id);
|
||||||
if (!node || !isWorkflowBlockNode(node)) {
|
if (!node || !isWorkflowBlockNode(node)) {
|
||||||
@@ -629,7 +526,11 @@ function FlowRenderer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
|
className="h-full w-full"
|
||||||
|
style={{ zIndex }}
|
||||||
|
onMouseDownCapture={() => onMouseDownCapture?.()}
|
||||||
|
>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={blocker.state === "blocked"}
|
open={blocker.state === "blocked"}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@@ -671,155 +572,83 @@ function FlowRenderer({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<WorkflowParametersStateContext.Provider
|
<BlockActionContext.Provider
|
||||||
value={[parameters, setParameters]}
|
value={{
|
||||||
|
deleteNodeCallback: deleteNode,
|
||||||
|
toggleScriptForNodeCallback: toggleScript,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<BlockActionContext.Provider
|
<ReactFlow
|
||||||
value={{
|
ref={editorElementRef}
|
||||||
deleteNodeCallback: deleteNode,
|
nodes={nodes}
|
||||||
toggleScriptForNodeCallback: toggleScript,
|
edges={edges}
|
||||||
|
onNodesChange={(changes) => {
|
||||||
|
const dimensionChanges = changes.filter(
|
||||||
|
(change) => change.type === "dimensions",
|
||||||
|
);
|
||||||
|
const tempNodes = [...nodes];
|
||||||
|
dimensionChanges.forEach((change) => {
|
||||||
|
const node = tempNodes.find((node) => node.id === change.id);
|
||||||
|
if (node) {
|
||||||
|
if (node.measured?.width) {
|
||||||
|
node.measured.width = change.dimensions?.width;
|
||||||
|
}
|
||||||
|
if (node.measured?.height) {
|
||||||
|
node.measured.height = change.dimensions?.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (dimensionChanges.length > 0) {
|
||||||
|
doLayout(tempNodes, edges);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
changes.some((change) => {
|
||||||
|
return (
|
||||||
|
change.type === "add" ||
|
||||||
|
change.type === "remove" ||
|
||||||
|
change.type === "replace"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
workflowChangesStore.setHasChanges(true);
|
||||||
|
}
|
||||||
|
// throttle onNodesChange to prevent cascading React updates
|
||||||
|
if (onNodesChangeTimeoutRef.current === null) {
|
||||||
|
onNodesChange(changes);
|
||||||
|
onNodesChangeTimeoutRef.current = setTimeout(() => {
|
||||||
|
onNodesChangeTimeoutRef.current = null;
|
||||||
|
}, 33); // ~30fps throttle
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
colorMode="dark"
|
||||||
|
fitView={true}
|
||||||
|
fitViewOptions={{
|
||||||
|
maxZoom: 1,
|
||||||
|
}}
|
||||||
|
deleteKeyCode={null}
|
||||||
|
onMove={(_, viewport) => {
|
||||||
|
if (debugStore.isDebugMode && shouldConstrainPan) {
|
||||||
|
constrainPan(viewport);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxZoom={debugStore.isDebugMode ? 1 : 2}
|
||||||
|
minZoom={debugStore.isDebugMode ? 1 : 0.5}
|
||||||
|
panOnDrag={true}
|
||||||
|
panOnScroll={true}
|
||||||
|
panOnScrollMode={PanOnScrollMode.Vertical}
|
||||||
|
zoomOnDoubleClick={!debugStore.isDebugMode}
|
||||||
|
zoomOnPinch={!debugStore.isDebugMode}
|
||||||
|
zoomOnScroll={!debugStore.isDebugMode}
|
||||||
>
|
>
|
||||||
<ReactFlow
|
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||||
ref={editorElementRef}
|
<Controls position="bottom-left" />
|
||||||
nodes={nodes}
|
</ReactFlow>
|
||||||
edges={edges}
|
</BlockActionContext.Provider>
|
||||||
onNodesChange={(changes) => {
|
</div>
|
||||||
const dimensionChanges = changes.filter(
|
|
||||||
(change) => change.type === "dimensions",
|
|
||||||
);
|
|
||||||
const tempNodes = [...nodes];
|
|
||||||
dimensionChanges.forEach((change) => {
|
|
||||||
const node = tempNodes.find((node) => node.id === change.id);
|
|
||||||
if (node) {
|
|
||||||
if (node.measured?.width) {
|
|
||||||
node.measured.width = change.dimensions?.width;
|
|
||||||
}
|
|
||||||
if (node.measured?.height) {
|
|
||||||
node.measured.height = change.dimensions?.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (dimensionChanges.length > 0) {
|
|
||||||
doLayout(tempNodes, edges);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
changes.some((change) => {
|
|
||||||
return (
|
|
||||||
change.type === "add" ||
|
|
||||||
change.type === "remove" ||
|
|
||||||
change.type === "replace"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
workflowChangesStore.setHasChanges(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// throttle onNodesChange to prevent cascading React updates
|
|
||||||
if (onNodesChangeTimeoutRef.current === null) {
|
|
||||||
onNodesChange(changes);
|
|
||||||
onNodesChangeTimeoutRef.current = setTimeout(() => {
|
|
||||||
onNodesChangeTimeoutRef.current = null;
|
|
||||||
}, 33); // ~30fps throttle
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
nodeTypes={nodeTypes}
|
|
||||||
edgeTypes={edgeTypes}
|
|
||||||
colorMode="dark"
|
|
||||||
fitView={true}
|
|
||||||
fitViewOptions={{
|
|
||||||
maxZoom: 1,
|
|
||||||
}}
|
|
||||||
deleteKeyCode={null}
|
|
||||||
onMove={(_, viewport) => {
|
|
||||||
if (debugStore.isDebugMode && shouldConstrainPan) {
|
|
||||||
constrainPan(viewport);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
maxZoom={debugStore.isDebugMode ? 1 : 2}
|
|
||||||
minZoom={debugStore.isDebugMode ? 1 : 0.5}
|
|
||||||
panOnDrag={true}
|
|
||||||
panOnScroll={true}
|
|
||||||
panOnScrollMode={PanOnScrollMode.Vertical}
|
|
||||||
zoomOnDoubleClick={!debugStore.isDebugMode}
|
|
||||||
zoomOnPinch={!debugStore.isDebugMode}
|
|
||||||
zoomOnScroll={!debugStore.isDebugMode}
|
|
||||||
>
|
|
||||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
|
||||||
<Controls position="bottom-left" />
|
|
||||||
{debugStore.isDebugMode && (
|
|
||||||
<Panel
|
|
||||||
position="top-right"
|
|
||||||
className="!bottom-[1rem] !right-[1.5rem] !top-0"
|
|
||||||
>
|
|
||||||
<div className="pointer-events-none absolute right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end">
|
|
||||||
<div className="pointer-events-auto relative mt-[8.5rem] h-full w-full overflow-hidden rounded-xl border-2 border-slate-500">
|
|
||||||
<WorkflowDebuggerRun />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
)}
|
|
||||||
<Panel position="top-center" className={cn("h-20")}>
|
|
||||||
<WorkflowHeader
|
|
||||||
debuggableBlockCount={debuggableBlockCount}
|
|
||||||
saving={workflowChangesStore.saveIsPending}
|
|
||||||
parametersPanelOpen={
|
|
||||||
workflowPanelState.active &&
|
|
||||||
workflowPanelState.content === "parameters"
|
|
||||||
}
|
|
||||||
onParametersClick={() => {
|
|
||||||
if (
|
|
||||||
workflowPanelState.active &&
|
|
||||||
workflowPanelState.content === "parameters"
|
|
||||||
) {
|
|
||||||
closeWorkflowPanel();
|
|
||||||
} else {
|
|
||||||
setWorkflowPanelState({
|
|
||||||
active: true,
|
|
||||||
content: "parameters",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSave={async () => {
|
|
||||||
const errors = getWorkflowErrors(nodes);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
toast({
|
|
||||||
title: "Can not save workflow because of errors:",
|
|
||||||
description: (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{errors.map((error) => (
|
|
||||||
<p key={error}>{error}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await handleSave();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Panel>
|
|
||||||
{workflowPanelState.active && (
|
|
||||||
<Panel position="top-right">
|
|
||||||
{workflowPanelState.content === "parameters" && (
|
|
||||||
<WorkflowParametersPanel />
|
|
||||||
)}
|
|
||||||
{workflowPanelState.content === "nodeLibrary" && (
|
|
||||||
<WorkflowNodeLibraryPanel
|
|
||||||
onNodeClick={(props) => {
|
|
||||||
addNode(props);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Panel>
|
|
||||||
)}
|
|
||||||
</ReactFlow>
|
|
||||||
</BlockActionContext.Provider>
|
|
||||||
</WorkflowParametersStateContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FlowRenderer };
|
export { FlowRenderer, type Props as FlowRendererProps };
|
||||||
|
|||||||
@@ -1,175 +1,18 @@
|
|||||||
import { AxiosError } from "axios";
|
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
|
|
||||||
import { getClient } from "@/api/AxiosClient";
|
|
||||||
import { DebugSessionApiResponse } from "@/api/types";
|
|
||||||
import { AnimatedWave } from "@/components/AnimatedWave";
|
|
||||||
import { BrowserStream } from "@/components/BrowserStream";
|
|
||||||
import { FloatingWindow } from "@/components/FloatingWindow";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogClose,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { toast } from "@/components/ui/use-toast";
|
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
|
||||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
|
||||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
|
||||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
|
||||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||||
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
|
||||||
import { WorkflowSettings } from "../types/workflowTypes";
|
import { WorkflowSettings } from "../types/workflowTypes";
|
||||||
import { FlowRenderer } from "./FlowRenderer";
|
|
||||||
import { getElements } from "./workflowEditorUtils";
|
import { getElements } from "./workflowEditorUtils";
|
||||||
import { getInitialParameters } from "./utils";
|
import { getInitialParameters } from "./utils";
|
||||||
|
import { Workspace } from "./Workspace";
|
||||||
const Constants = {
|
|
||||||
NewBrowserCooldown: 30000,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function WorkflowDebugger() {
|
function WorkflowDebugger() {
|
||||||
const { blockLabel, workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
const [openDialogue, setOpenDialogue] = useState(false);
|
|
||||||
const [activeDebugSession, setActiveDebugSession] =
|
|
||||||
useState<DebugSessionApiResponse | null>(null);
|
|
||||||
const [showPowerButton, setShowPowerButton] = useState(true);
|
|
||||||
const credentialGetter = useCredentialGetter();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
|
||||||
const blockScriptStore = useBlockScriptStore();
|
|
||||||
|
|
||||||
const { data: workflowRun } = useWorkflowRunQuery();
|
|
||||||
|
|
||||||
const { data: workflow } = useWorkflowQuery({
|
const { data: workflow } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: blockScripts } = useBlockScriptsQuery({
|
|
||||||
workflowPermanentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: debugSession } = useDebugSessionQuery({
|
|
||||||
workflowPermanentId,
|
|
||||||
enabled: shouldFetchDebugSession && !!workflowPermanentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setCollapsed = useSidebarStore((state) => {
|
|
||||||
return state.setCollapsed;
|
|
||||||
});
|
|
||||||
|
|
||||||
const workflowChangesStore = useWorkflowHasChangesStore();
|
|
||||||
|
|
||||||
const handleOnCycle = () => {
|
|
||||||
setOpenDialogue(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useMountEffect(() => {
|
|
||||||
setCollapsed(true);
|
|
||||||
workflowChangesStore.setHasChanges(false);
|
|
||||||
|
|
||||||
if (workflowPermanentId) {
|
|
||||||
queryClient.removeQueries({
|
|
||||||
queryKey: ["debugSession", workflowPermanentId],
|
|
||||||
});
|
|
||||||
setShouldFetchDebugSession(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
blockScriptStore.setScripts(blockScripts ?? {});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [blockScripts]);
|
|
||||||
|
|
||||||
const afterCycleBrowser = () => {
|
|
||||||
setOpenDialogue(false);
|
|
||||||
setShowPowerButton(false);
|
|
||||||
|
|
||||||
if (powerButtonTimeoutRef.current) {
|
|
||||||
clearTimeout(powerButtonTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
powerButtonTimeoutRef.current = setTimeout(() => {
|
|
||||||
setShowPowerButton(true);
|
|
||||||
}, Constants.NewBrowserCooldown);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cycleBrowser = useMutation({
|
|
||||||
mutationFn: async (id: string) => {
|
|
||||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
|
||||||
return client.post<DebugSessionApiResponse>(`/debug-session/${id}/new`);
|
|
||||||
},
|
|
||||||
onSuccess: (response) => {
|
|
||||||
const newDebugSession = response.data;
|
|
||||||
setActiveDebugSession(newDebugSession);
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["debugSession", workflowPermanentId],
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Browser cycled",
|
|
||||||
variant: "success",
|
|
||||||
description: "Your browser has been cycled.",
|
|
||||||
});
|
|
||||||
|
|
||||||
afterCycleBrowser();
|
|
||||||
},
|
|
||||||
onError: (error: AxiosError) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Failed to cycle browser",
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
afterCycleBrowser();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const powerButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(!debugSession || !debugSession.browser_session_id) &&
|
|
||||||
shouldFetchDebugSession &&
|
|
||||||
workflowPermanentId
|
|
||||||
) {
|
|
||||||
intervalRef.current = setInterval(() => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["debugSession", workflowPermanentId],
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugSession) {
|
|
||||||
setActiveDebugSession(debugSession);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]);
|
|
||||||
|
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -193,110 +36,18 @@ function WorkflowDebugger() {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
|
||||||
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
|
|
||||||
const browserTitle = interactor === "agent" ? `Browser [🤖]` : `Browser [👤]`;
|
|
||||||
|
|
||||||
// ---start fya: https://github.com/frontyardart
|
|
||||||
const initialBrowserPosition = {
|
|
||||||
x: 600,
|
|
||||||
y: 132,
|
|
||||||
};
|
|
||||||
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const rightPadding = 567;
|
|
||||||
const initialWidth = Math.max(
|
|
||||||
512,
|
|
||||||
windowWidth - initialBrowserPosition.x - rightPadding,
|
|
||||||
);
|
|
||||||
const initialHeight = (initialWidth / 16) * 9;
|
|
||||||
// ---end fya
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen w-full">
|
<div className="relative flex h-screen w-full">
|
||||||
<Dialog
|
|
||||||
open={openDialogue}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open && cycleBrowser.isPending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpenDialogue(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Cycle (Get a new browser)</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<div className="pb-2 pt-4 text-sm text-slate-400">
|
|
||||||
{cycleBrowser.isPending ? (
|
|
||||||
<>
|
|
||||||
Cooking you up a fresh browser...
|
|
||||||
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Abandon this browser for a new one. Are you sure?"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
{!cycleBrowser.isPending && (
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="secondary">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
onClick={() => {
|
|
||||||
cycleBrowser.mutate(workflowPermanentId!);
|
|
||||||
}}
|
|
||||||
disabled={cycleBrowser.isPending}
|
|
||||||
>
|
|
||||||
Yes, Continue{" "}
|
|
||||||
{cycleBrowser.isPending && (
|
|
||||||
<ReloadIcon className="ml-2 size-4 animate-spin" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<FlowRenderer
|
<Workspace
|
||||||
initialEdges={elements.edges}
|
initialEdges={elements.edges}
|
||||||
initialNodes={elements.nodes}
|
initialNodes={elements.nodes}
|
||||||
initialParameters={getInitialParameters(workflow)}
|
initialParameters={getInitialParameters(workflow)}
|
||||||
initialTitle={workflow.title}
|
initialTitle={workflow.title}
|
||||||
|
showBrowser={true}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
/>
|
/>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
|
||||||
<FloatingWindow
|
|
||||||
title={browserTitle}
|
|
||||||
bounded={false}
|
|
||||||
initialPosition={initialBrowserPosition}
|
|
||||||
initialWidth={initialWidth}
|
|
||||||
initialHeight={initialHeight}
|
|
||||||
showMaximizeButton={true}
|
|
||||||
showMinimizeButton={true}
|
|
||||||
showPowerButton={blockLabel === undefined && showPowerButton}
|
|
||||||
showReloadButton={true}
|
|
||||||
// --
|
|
||||||
onCycle={handleOnCycle}
|
|
||||||
>
|
|
||||||
{activeDebugSession &&
|
|
||||||
activeDebugSession.browser_session_id &&
|
|
||||||
!cycleBrowser.isPending ? (
|
|
||||||
<BrowserStream
|
|
||||||
interactive={interactor === "human"}
|
|
||||||
browserSessionId={activeDebugSession.browser_session_id}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 pb-2 pt-4 text-sm text-slate-400">
|
|
||||||
Connecting to your browser...
|
|
||||||
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FloatingWindow>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
|
||||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
|
||||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||||
import { FlowRenderer } from "./FlowRenderer";
|
|
||||||
import { getElements } from "./workflowEditorUtils";
|
import { getElements } from "./workflowEditorUtils";
|
||||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||||
import { WorkflowSettings } from "../types/workflowTypes";
|
import { WorkflowSettings } from "../types/workflowTypes";
|
||||||
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||||
import { getInitialParameters } from "./utils";
|
import { getInitialParameters } from "./utils";
|
||||||
|
import { Workspace } from "./Workspace";
|
||||||
|
|
||||||
function WorkflowEditor() {
|
function WorkflowEditor() {
|
||||||
const { workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
const setCollapsed = useSidebarStore((state) => {
|
|
||||||
return state.setCollapsed;
|
|
||||||
});
|
|
||||||
const workflowChangesStore = useWorkflowHasChangesStore();
|
|
||||||
const blockScriptStore = useBlockScriptStore();
|
|
||||||
|
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
@@ -29,20 +18,6 @@ function WorkflowEditor() {
|
|||||||
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
|
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
|
||||||
useGlobalWorkflowsQuery();
|
useGlobalWorkflowsQuery();
|
||||||
|
|
||||||
const { data: blockScripts } = useBlockScriptsQuery({
|
|
||||||
workflowPermanentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
useMountEffect(() => {
|
|
||||||
setCollapsed(true);
|
|
||||||
workflowChangesStore.setHasChanges(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
blockScriptStore.setScripts(blockScripts ?? {});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [blockScripts]);
|
|
||||||
|
|
||||||
if (isLoading || isGlobalWorkflowsLoading) {
|
if (isLoading || isGlobalWorkflowsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center">
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
@@ -82,11 +57,12 @@ function WorkflowEditor() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen w-full">
|
<div className="relative flex h-screen w-full">
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<FlowRenderer
|
<Workspace
|
||||||
initialEdges={elements.edges}
|
initialEdges={elements.edges}
|
||||||
initialNodes={elements.nodes}
|
initialNodes={elements.nodes}
|
||||||
initialParameters={getInitialParameters(workflow)}
|
initialParameters={getInitialParameters(workflow)}
|
||||||
initialTitle={workflow.title}
|
initialTitle={workflow.title}
|
||||||
|
showBrowser={false}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
/>
|
/>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Props = {
|
|||||||
parametersPanelOpen: boolean;
|
parametersPanelOpen: boolean;
|
||||||
onParametersClick: () => void;
|
onParametersClick: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
|
onRun?: () => void;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ function WorkflowHeader({
|
|||||||
parametersPanelOpen,
|
parametersPanelOpen,
|
||||||
onParametersClick,
|
onParametersClick,
|
||||||
onSave,
|
onSave,
|
||||||
|
onRun,
|
||||||
saving,
|
saving,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { title, setTitle } = useWorkflowTitleStore();
|
const { title, setTitle } = useWorkflowTitleStore();
|
||||||
@@ -148,6 +150,7 @@ function WorkflowHeader({
|
|||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
onRun?.();
|
||||||
navigate(`/workflows/${workflowPermanentId}/run`);
|
navigate(`/workflows/${workflowPermanentId}/run`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
543
skyvern-frontend/src/routes/workflows/editor/Workspace.tsx
Normal file
543
skyvern-frontend/src/routes/workflows/editor/Workspace.tsx
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { DebugSessionApiResponse } from "@/api/types";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
|
import { useRanker } from "../hooks/useRanker";
|
||||||
|
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
||||||
|
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||||
|
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||||
|
import { useSidebarStore } from "@/store/SidebarStore";
|
||||||
|
|
||||||
|
import { AnimatedWave } from "@/components/AnimatedWave";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { BrowserStream } from "@/components/BrowserStream";
|
||||||
|
import { FloatingWindow } from "@/components/FloatingWindow";
|
||||||
|
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||||
|
import { WorkflowDebuggerRun } from "@/routes/workflows/editor/WorkflowDebuggerRun";
|
||||||
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
|
import { useDebugStore } from "@/store/useDebugStore";
|
||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
import {
|
||||||
|
useWorkflowHasChangesStore,
|
||||||
|
useWorkflowSave,
|
||||||
|
} from "@/store/WorkflowHasChangesStore";
|
||||||
|
|
||||||
|
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
||||||
|
import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
|
||||||
|
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||||
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
|
import { ParametersState } from "./types";
|
||||||
|
import { getWorkflowErrors } from "./workflowEditorUtils";
|
||||||
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
|
import {
|
||||||
|
nodeAdderNode,
|
||||||
|
createNode,
|
||||||
|
defaultEdge,
|
||||||
|
generateNodeLabel,
|
||||||
|
layout,
|
||||||
|
startNode,
|
||||||
|
} from "./workflowEditorUtils";
|
||||||
|
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||||
|
|
||||||
|
const Constants = {
|
||||||
|
NewBrowserCooldown: 30000,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Props = Pick<
|
||||||
|
FlowRendererProps,
|
||||||
|
"initialTitle" | "initialParameters" | "workflow"
|
||||||
|
> & {
|
||||||
|
initialNodes: Array<AppNode>;
|
||||||
|
initialEdges: Array<Edge>;
|
||||||
|
showBrowser?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddNodeProps = {
|
||||||
|
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||||
|
previous: string | null;
|
||||||
|
next: string | null;
|
||||||
|
parent?: string;
|
||||||
|
connectingEdgeType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Workspace({
|
||||||
|
initialNodes,
|
||||||
|
initialEdges,
|
||||||
|
initialTitle,
|
||||||
|
initialParameters,
|
||||||
|
showBrowser = false,
|
||||||
|
workflow,
|
||||||
|
}: Props) {
|
||||||
|
const { blockLabel, workflowPermanentId } = useParams();
|
||||||
|
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
||||||
|
useWorkflowPanelStore();
|
||||||
|
const [parameters, setParameters] =
|
||||||
|
useState<ParametersState>(initialParameters);
|
||||||
|
const debugStore = useDebugStore();
|
||||||
|
const [debuggableBlockCount, setDebuggableBlockCount] = useState(0);
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const saveWorkflow = useWorkflowSave();
|
||||||
|
|
||||||
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
|
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
||||||
|
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
|
||||||
|
const browserTitle = interactor === "agent" ? `Browser [🤖]` : `Browser [👤]`;
|
||||||
|
|
||||||
|
const [openDialogue, setOpenDialogue] = useState(false);
|
||||||
|
const [activeDebugSession, setActiveDebugSession] =
|
||||||
|
useState<DebugSessionApiResponse | null>(null);
|
||||||
|
const [showPowerButton, setShowPowerButton] = useState(true);
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
||||||
|
const blockScriptStore = useBlockScriptStore();
|
||||||
|
const { rankedItems, promote } = useRanker([
|
||||||
|
"browserWindow",
|
||||||
|
"header",
|
||||||
|
"dropdown",
|
||||||
|
"history",
|
||||||
|
"infiniteCanvas",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ---start fya: https://github.com/frontyardart
|
||||||
|
const initialBrowserPosition = {
|
||||||
|
x: 600,
|
||||||
|
y: 132,
|
||||||
|
};
|
||||||
|
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const rightPadding = 567;
|
||||||
|
const initialWidth = Math.max(
|
||||||
|
512,
|
||||||
|
windowWidth - initialBrowserPosition.x - rightPadding,
|
||||||
|
);
|
||||||
|
const initialHeight = (initialWidth / 16) * 9;
|
||||||
|
// ---end fya
|
||||||
|
|
||||||
|
const { data: blockScripts } = useBlockScriptsQuery({
|
||||||
|
workflowPermanentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: debugSession } = useDebugSessionQuery({
|
||||||
|
workflowPermanentId,
|
||||||
|
enabled: shouldFetchDebugSession && !!workflowPermanentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setCollapsed = useSidebarStore((state) => {
|
||||||
|
return state.setCollapsed;
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
|
|
||||||
|
const handleOnCycle = () => {
|
||||||
|
setOpenDialogue(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useMountEffect(() => {
|
||||||
|
setCollapsed(true);
|
||||||
|
workflowChangesStore.setHasChanges(false);
|
||||||
|
|
||||||
|
if (workflowPermanentId) {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: ["debugSession", workflowPermanentId],
|
||||||
|
});
|
||||||
|
setShouldFetchDebugSession(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
blockScriptStore.setScripts(blockScripts ?? {});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [blockScripts]);
|
||||||
|
|
||||||
|
const afterCycleBrowser = () => {
|
||||||
|
setOpenDialogue(false);
|
||||||
|
setShowPowerButton(false);
|
||||||
|
|
||||||
|
if (powerButtonTimeoutRef.current) {
|
||||||
|
clearTimeout(powerButtonTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
powerButtonTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowPowerButton(true);
|
||||||
|
}, Constants.NewBrowserCooldown);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cycleBrowser = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
|
return client.post<DebugSessionApiResponse>(`/debug-session/${id}/new`);
|
||||||
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const newDebugSession = response.data;
|
||||||
|
setActiveDebugSession(newDebugSession);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["debugSession", workflowPermanentId],
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Browser cycled",
|
||||||
|
variant: "success",
|
||||||
|
description: "Your browser has been cycled.",
|
||||||
|
});
|
||||||
|
|
||||||
|
afterCycleBrowser();
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to cycle browser",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
afterCycleBrowser();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const powerButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
(!debugSession || !debugSession.browser_session_id) &&
|
||||||
|
shouldFetchDebugSession &&
|
||||||
|
workflowPermanentId
|
||||||
|
) {
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["debugSession", workflowPermanentId],
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugSession) {
|
||||||
|
setActiveDebugSession(debugSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]);
|
||||||
|
|
||||||
|
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
||||||
|
const layoutedElements = layout(nodes, edges);
|
||||||
|
setNodes(layoutedElements.nodes);
|
||||||
|
setEdges(layoutedElements.edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode({
|
||||||
|
nodeType,
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
parent,
|
||||||
|
connectingEdgeType,
|
||||||
|
}: AddNodeProps) {
|
||||||
|
const newNodes: Array<AppNode> = [];
|
||||||
|
const newEdges: Array<Edge> = [];
|
||||||
|
const id = nanoid();
|
||||||
|
const existingLabels = nodes
|
||||||
|
.filter(isWorkflowBlockNode)
|
||||||
|
.map((node) => node.data.label);
|
||||||
|
const node = createNode(
|
||||||
|
{ id, parentId: parent },
|
||||||
|
nodeType,
|
||||||
|
generateNodeLabel(existingLabels),
|
||||||
|
);
|
||||||
|
newNodes.push(node);
|
||||||
|
if (previous) {
|
||||||
|
const newEdge = {
|
||||||
|
id: nanoid(),
|
||||||
|
type: "edgeWithAddButton",
|
||||||
|
source: previous,
|
||||||
|
target: id,
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
const newEdge = {
|
||||||
|
id: nanoid(),
|
||||||
|
type: connectingEdgeType,
|
||||||
|
source: id,
|
||||||
|
target: next,
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeType === "loop") {
|
||||||
|
// when loop node is first created it needs an adder node so nodes can be added inside the loop
|
||||||
|
const startNodeId = nanoid();
|
||||||
|
const adderNodeId = nanoid();
|
||||||
|
newNodes.push(
|
||||||
|
startNode(
|
||||||
|
startNodeId,
|
||||||
|
{
|
||||||
|
withWorkflowSettings: false,
|
||||||
|
editable: true,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
newNodes.push(nodeAdderNode(adderNodeId, id));
|
||||||
|
newEdges.push(defaultEdge(startNodeId, adderNodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const editedEdges = previous
|
||||||
|
? edges.filter((edge) => edge.source !== previous)
|
||||||
|
: edges;
|
||||||
|
|
||||||
|
const previousNode = nodes.find((node) => node.id === previous);
|
||||||
|
const previousNodeIndex = previousNode
|
||||||
|
? nodes.indexOf(previousNode)
|
||||||
|
: nodes.length - 1;
|
||||||
|
|
||||||
|
// creating some memory for no reason, maybe check it out later
|
||||||
|
const newNodesAfter = [
|
||||||
|
...nodes.slice(0, previousNodeIndex + 1),
|
||||||
|
...newNodes,
|
||||||
|
...nodes.slice(previousNodeIndex + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
workflowChangesStore.setHasChanges(true);
|
||||||
|
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowParametersStateContext.Provider
|
||||||
|
value={[parameters, setParameters]}
|
||||||
|
>
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
<Dialog
|
||||||
|
open={openDialogue}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && cycleBrowser.isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpenDialogue(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Cycle (Get a new browser)</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<div className="pb-2 pt-4 text-sm text-slate-400">
|
||||||
|
{cycleBrowser.isPending ? (
|
||||||
|
<>
|
||||||
|
Cooking you up a fresh browser...
|
||||||
|
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Abandon this browser for a new one. Are you sure?"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
{!cycleBrowser.isPending && (
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
cycleBrowser.mutate(workflowPermanentId!);
|
||||||
|
}}
|
||||||
|
disabled={cycleBrowser.isPending}
|
||||||
|
>
|
||||||
|
Yes, Continue{" "}
|
||||||
|
{cycleBrowser.isPending && (
|
||||||
|
<ReloadIcon className="ml-2 size-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* header panel */}
|
||||||
|
<div
|
||||||
|
className="absolute left-6 right-6 top-8 h-20"
|
||||||
|
style={{ zIndex: rankedItems.header ?? 3 }}
|
||||||
|
onMouseDownCapture={() => {
|
||||||
|
promote("header");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WorkflowHeader
|
||||||
|
debuggableBlockCount={debuggableBlockCount}
|
||||||
|
saving={workflowChangesStore.saveIsPending}
|
||||||
|
parametersPanelOpen={
|
||||||
|
workflowPanelState.active &&
|
||||||
|
workflowPanelState.content === "parameters"
|
||||||
|
}
|
||||||
|
onParametersClick={() => {
|
||||||
|
if (
|
||||||
|
workflowPanelState.active &&
|
||||||
|
workflowPanelState.content === "parameters"
|
||||||
|
) {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
promote("header");
|
||||||
|
} else {
|
||||||
|
setWorkflowPanelState({
|
||||||
|
active: true,
|
||||||
|
content: "parameters",
|
||||||
|
});
|
||||||
|
promote("dropdown");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSave={async () => {
|
||||||
|
const errors = getWorkflowErrors(nodes);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "Can not save workflow because of errors:",
|
||||||
|
description: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error}>{error}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await saveWorkflow.mutateAsync();
|
||||||
|
}}
|
||||||
|
onRun={() => {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
promote("header");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* sub panels */}
|
||||||
|
{workflowPanelState.active && (
|
||||||
|
<div
|
||||||
|
className="absolute right-6 top-[7.75rem]"
|
||||||
|
style={{ zIndex: rankedItems.dropdown ?? 2 }}
|
||||||
|
onMouseDownCapture={() => {
|
||||||
|
promote("dropdown");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{workflowPanelState.content === "parameters" && (
|
||||||
|
<WorkflowParametersPanel
|
||||||
|
onMouseDownCapture={() => {
|
||||||
|
promote("dropdown");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onMouseDownCapture={() => {
|
||||||
|
promote("dropdown");
|
||||||
|
}}
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{debugStore.isDebugMode && (
|
||||||
|
<div
|
||||||
|
className="absolute right-6 top-[8.5rem] h-[calc(100vh-9.5rem)]"
|
||||||
|
style={{ zIndex: rankedItems.history ?? 1 }}
|
||||||
|
onMouseDownCapture={() => {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
promote("history");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="pointer-events-none absolute right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end">
|
||||||
|
<div className="pointer-events-auto relative h-full w-full overflow-hidden rounded-xl border-2 border-slate-500">
|
||||||
|
<WorkflowDebuggerRun />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* infinite canvas */}
|
||||||
|
<FlowRenderer
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
setNodes={setNodes}
|
||||||
|
setEdges={setEdges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
initialTitle={initialTitle}
|
||||||
|
initialParameters={initialParameters}
|
||||||
|
workflow={workflow}
|
||||||
|
onDebuggableBlockCountChange={(c) => setDebuggableBlockCount(c)}
|
||||||
|
onMouseDownCapture={() => promote("infiniteCanvas")}
|
||||||
|
zIndex={rankedItems.infiniteCanvas}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* browser */}
|
||||||
|
{showBrowser && (
|
||||||
|
<FloatingWindow
|
||||||
|
title={browserTitle}
|
||||||
|
bounded={false}
|
||||||
|
initialPosition={initialBrowserPosition}
|
||||||
|
initialWidth={initialWidth}
|
||||||
|
initialHeight={initialHeight}
|
||||||
|
showMaximizeButton={true}
|
||||||
|
showMinimizeButton={true}
|
||||||
|
showPowerButton={blockLabel === undefined && showPowerButton}
|
||||||
|
showReloadButton={true}
|
||||||
|
zIndex={rankedItems.browserWindow ?? 4}
|
||||||
|
// --
|
||||||
|
onCycle={handleOnCycle}
|
||||||
|
onFocus={() => promote("browserWindow")}
|
||||||
|
>
|
||||||
|
{activeDebugSession &&
|
||||||
|
activeDebugSession.browser_session_id &&
|
||||||
|
!cycleBrowser.isPending ? (
|
||||||
|
<BrowserStream
|
||||||
|
interactive={interactor === "human"}
|
||||||
|
browserSessionId={activeDebugSession.browser_session_id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center gap-2 pb-2 pt-4 text-sm text-slate-400">
|
||||||
|
Connecting to your browser...
|
||||||
|
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FloatingWindow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</WorkflowParametersStateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Workspace };
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/routes/workflows/types/workflowTypes";
|
} from "@/routes/workflows/types/workflowTypes";
|
||||||
import { getInitialValues } from "@/routes/workflows/utils";
|
import { getInitialValues } from "@/routes/workflows/utils";
|
||||||
import { useDebugStore } from "@/store/useDebugStore";
|
import { useDebugStore } from "@/store/useDebugStore";
|
||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
import { useWorkflowSave } from "@/store/WorkflowHasChangesStore";
|
import { useWorkflowSave } from "@/store/WorkflowHasChangesStore";
|
||||||
import {
|
import {
|
||||||
useWorkflowSettingsStore,
|
useWorkflowSettingsStore,
|
||||||
@@ -135,6 +136,7 @@ function NodeHeader({
|
|||||||
workflowRunId,
|
workflowRunId,
|
||||||
} = useParams();
|
} = useParams();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
|
const { closeWorkflowPanel } = useWorkflowPanelStore();
|
||||||
const thisBlockIsPlaying =
|
const thisBlockIsPlaying =
|
||||||
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
|
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
|
||||||
const anyBlockIsPlaying =
|
const anyBlockIsPlaying =
|
||||||
@@ -194,6 +196,8 @@ function NodeHeader({
|
|||||||
|
|
||||||
const runBlock = useMutation({
|
const runBlock = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
|
||||||
await saveWorkflow.mutateAsync();
|
await saveWorkflow.mutateAsync();
|
||||||
|
|
||||||
if (!workflowPermanentId) {
|
if (!workflowPermanentId) {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { WorkflowBlockTypes } from "../../types/workflowTypes";
|
import { WorkflowBlockTypes } from "../../types/workflowTypes";
|
||||||
import { AddNodeProps } from "../FlowRenderer";
|
|
||||||
import { WorkflowBlockNode } from "../nodes";
|
import { WorkflowBlockNode } from "../nodes";
|
||||||
import { WorkflowBlockIcon } from "../nodes/WorkflowBlockIcon";
|
import { WorkflowBlockIcon } from "../nodes/WorkflowBlockIcon";
|
||||||
|
import { AddNodeProps } from "../Workspace";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
const enableCodeBlock =
|
const enableCodeBlock =
|
||||||
@@ -243,11 +243,16 @@ const nodeLibraryItems: Array<{
|
|||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
onMouseDownCapture?: () => void;
|
||||||
onNodeClick: (props: AddNodeProps) => void;
|
onNodeClick: (props: AddNodeProps) => void;
|
||||||
first?: boolean;
|
first?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) {
|
function WorkflowNodeLibraryPanel({
|
||||||
|
onMouseDownCapture,
|
||||||
|
onNodeClick,
|
||||||
|
first,
|
||||||
|
}: Props) {
|
||||||
const workflowPanelData = useWorkflowPanelStore(
|
const workflowPanelData = useWorkflowPanelStore(
|
||||||
(state) => state.workflowPanelState.data,
|
(state) => state.workflowPanelState.data,
|
||||||
);
|
);
|
||||||
@@ -311,7 +316,10 @@ function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
<div
|
||||||
|
className="w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
||||||
|
onMouseDownCapture={() => onMouseDownCapture?.()}
|
||||||
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<header className="space-y-2">
|
<header className="space-y-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ import { getLabelForWorkflowParameterType } from "../workflowEditorUtils";
|
|||||||
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
||||||
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
||||||
|
|
||||||
function WorkflowParametersPanel() {
|
interface Props {
|
||||||
|
onMouseDownCapture?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowParametersPanel({ onMouseDownCapture }: Props) {
|
||||||
const setHasChanges = useWorkflowHasChangesStore(
|
const setHasChanges = useWorkflowHasChangesStore(
|
||||||
(state) => state.setHasChanges,
|
(state) => state.setHasChanges,
|
||||||
);
|
);
|
||||||
@@ -56,7 +60,10 @@ function WorkflowParametersPanel() {
|
|||||||
const { setNodes } = useReactFlow();
|
const { setNodes } = useReactFlow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
<div
|
||||||
|
className="relative z-10 w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
||||||
|
onMouseDownCapture={() => onMouseDownCapture?.()}
|
||||||
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="text-lg">Parameters</h1>
|
<h1 className="text-lg">Parameters</h1>
|
||||||
|
|||||||
46
skyvern-frontend/src/routes/workflows/hooks/useRanker.ts
Normal file
46
skyvern-frontend/src/routes/workflows/hooks/useRanker.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
|
||||||
|
interface RankedItems {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseRankerReturn {
|
||||||
|
rankedItems: RankedItems;
|
||||||
|
promote: (name: string) => void;
|
||||||
|
orderedNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRanker(initialNames: string[]): UseRankerReturn {
|
||||||
|
const [orderedNames, setOrderedNames] = useState<string[]>(initialNames);
|
||||||
|
|
||||||
|
const rankedItems = useMemo<RankedItems>(() => {
|
||||||
|
const items: RankedItems = {};
|
||||||
|
const maxRank = orderedNames.length;
|
||||||
|
|
||||||
|
orderedNames.forEach((name, index) => {
|
||||||
|
items[name] = maxRank - index;
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [orderedNames]);
|
||||||
|
|
||||||
|
const promote = useCallback((name: string) => {
|
||||||
|
setOrderedNames((prevNames) => {
|
||||||
|
if (!prevNames.includes(name)) {
|
||||||
|
console.warn(`Name "${name}" not found in ranked list`);
|
||||||
|
return prevNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredNames = prevNames.filter((n) => n !== name);
|
||||||
|
return [name, ...filteredNames];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rankedItems,
|
||||||
|
promote,
|
||||||
|
orderedNames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useRanker, type RankedItems };
|
||||||
Reference in New Issue
Block a user