Feature: workflow history, buttons moved on top (#3492)

This commit is contained in:
Alex Angin
2025-09-24 09:37:49 -04:00
committed by GitHub
parent b2848ceaf2
commit 38dfd00d9d
3 changed files with 309 additions and 227 deletions

View File

@@ -1,5 +1,11 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useEffect, useRef, useState, MutableRefObject } from "react"; import {
useCallback,
useEffect,
useRef,
useState,
MutableRefObject,
} from "react";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { import {
ChevronRightIcon, ChevronRightIcon,
@@ -300,6 +306,33 @@ function Workspace({
closeWorkflowPanel(); closeWorkflowPanel();
}); });
// Centralized function to manage comparison and panel states
const clearComparisonViewAndShowFreshIfActive = useCallback(
(active: boolean) => {
setWorkflowPanelState({
active,
content: "history",
data: {
showComparison: false,
version1: undefined,
version2: undefined,
},
});
},
[setWorkflowPanelState],
);
// Clear comparison view when switching between browser mode and editor mode
useEffect(() => {
if (workflowPanelState.data?.showComparison) {
clearComparisonViewAndShowFreshIfActive(false);
setShowAllCode(false);
}
// We intentionally omit workflowPanelState.data?.showComparison from deps
// to avoid clearing comparison immediately when it's set
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showBrowser, clearComparisonViewAndShowFreshIfActive]);
useMountEffect(() => { useMountEffect(() => {
const closePanelsWhenEscapeIsPressed = (e: KeyboardEvent) => { const closePanelsWhenEscapeIsPressed = (e: KeyboardEvent) => {
if (e.key === "Escape") { if (e.key === "Escape") {
@@ -572,19 +605,6 @@ function Workspace({
} }
} }
// Centralized function to manage comparison and panel states
function clearComparisonViewAndShowFreshIfActive(active: boolean) {
setWorkflowPanelState({
active,
content: "history",
data: {
showComparison: false,
version1: undefined,
version2: undefined,
},
});
}
function toggleHistoryPanel() { function toggleHistoryPanel() {
// Capture current state before making changes // Capture current state before making changes
const wasInComparisonMode = workflowPanelState.data?.showComparison; const wasInComparisonMode = workflowPanelState.data?.showComparison;
@@ -895,13 +915,12 @@ function Workspace({
/> />
</div> </div>
{/* infinite canvas and sub panels when not in debug mode */} {/* comparison view (takes precedence over both browser and non-browser modes) */}
{!showBrowser && (
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
{/* infinite canvas or comparison view */}
{workflowPanelState.data?.showComparison && {workflowPanelState.data?.showComparison &&
workflowPanelState.data?.version1 && workflowPanelState.data?.version1 &&
workflowPanelState.data?.version2 ? ( workflowPanelState.data?.version2 ? (
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
{/* comparison view */}
<div <div
className="absolute left-6 top-[6rem]" className="absolute left-6 top-[6rem]"
style={{ style={{
@@ -916,7 +935,68 @@ function Workspace({
onSelectState={handleSelectState} onSelectState={handleSelectState}
/> />
</div> </div>
{/* sub panels */}
{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" && (
<div className="z-30">
<WorkflowParametersPanel />
</div>
)}
{workflowPanelState.content === "history" && (
<div className="pointer-events-auto relative right-0 top-[3.5rem] z-30 h-[calc(100vh-14rem)]">
<WorkflowHistoryPanel
workflowPermanentId={workflowPermanentId!}
onCompare={handleCompareVersions}
/>
</div>
)}
{workflowPanelState.content === "nodeLibrary" && (
<div className="z-30 h-full w-[25rem]">
<WorkflowNodeLibraryPanel
onNodeClick={(props) => {
addNode(props);
}}
/>
</div>
)}
</div>
)}
</div>
) : ( ) : (
<>
{/* infinite canvas and sub panels when not in debug mode */}
{!showBrowser && (
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
{/* infinite canvas */}
<FlowRenderer <FlowRenderer
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
@@ -927,7 +1007,6 @@ function Workspace({
initialTitle={initialTitle} initialTitle={initialTitle}
workflow={workflow} workflow={workflow}
/> />
)}
{/* sub panels */} {/* sub panels */}
{workflowPanelState.active && ( {workflowPanelState.active && (
@@ -985,9 +1064,13 @@ function Workspace({
)} )}
</div> </div>
)} )}
</>
)}
{/* sub panels when in debug mode */} {/* sub panels when in debug mode */}
{showBrowser && workflowPanelState.active && ( {showBrowser &&
!workflowPanelState.data?.showComparison &&
workflowPanelState.active && (
<div <div
className="absolute right-6 top-[8.5rem] z-30" className="absolute right-6 top-[8.5rem] z-30"
style={{ style={{
@@ -1023,7 +1106,7 @@ function Workspace({
)} )}
{/* code, infinite canvas, browser, and timeline when in debug mode */} {/* code, infinite canvas, browser, and timeline when in debug mode */}
{showBrowser && ( {showBrowser && !workflowPanelState.data?.showComparison && (
<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"
@@ -1032,25 +1115,8 @@ function Workspace({
split={{ left: workflowWidth }} split={{ left: workflowWidth }}
onResize={() => setContainerResizeTrigger((prev) => prev + 1)} onResize={() => setContainerResizeTrigger((prev) => prev + 1)}
> >
{/* code and infinite canvas or comparison view */} {/* code and infinite canvas */}
<div className="relative h-full w-full"> <div className="relative h-full w-full">
{workflowPanelState.data?.showComparison &&
workflowPanelState.data?.version1 &&
workflowPanelState.data?.version2 ? (
<div
className="absolute inset-0 top-[8.5rem] p-6"
style={{
height: "calc(100vh - 14.5rem)",
}}
>
<WorkflowComparisonPanel
key={`${workflowPanelState.data.version1.workflow_id}v${workflowPanelState.data.version1.version}-${workflowPanelState.data.version2.workflow_id}v${workflowPanelState.data.version2.version}`}
version1={workflowPanelState.data.version1}
version2={workflowPanelState.data.version2}
onSelectState={handleSelectState}
/>
</div>
) : (
<div <div
className={cn( className={cn(
"skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300", "skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300",
@@ -1107,7 +1173,6 @@ function Workspace({
/> />
</div> </div>
</div> </div>
)}
</div> </div>
{/* browser & timeline */} {/* browser & timeline */}

View File

@@ -1,5 +1,4 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
@@ -171,7 +170,6 @@ function getWorkflowElements(version: WorkflowVersion) {
function WorkflowComparisonRenderer({ function WorkflowComparisonRenderer({
version, version,
onSelectState,
blockColors, blockColors,
}: { }: {
version: WorkflowVersion; version: WorkflowVersion;
@@ -239,32 +237,7 @@ function WorkflowComparisonRenderer({
); );
return ( return (
<div className="h-full w-full"> <div className="h-full w-full rounded-lg border bg-white">
<div className="mb-3 flex flex-col items-center justify-center gap-2">
<div className="text-center">
<div className="mb-1 flex items-center justify-center gap-2">
<Badge variant="secondary">
{version.title}, version: {version.version}
</Badge>
<Badge variant="secondary">
{version.workflow_definition?.blocks?.length || 0} block
{(version.workflow_definition?.blocks?.length || 0) !== 1
? "s"
: ""}
</Badge>
{onSelectState && (
<Button
size="sm"
onClick={() => onSelectState(version)}
className="text-xs"
>
Select this state
</Button>
)}
</div>
</div>
</div>
<div className="h-[calc(100%-4rem)] rounded-lg border bg-white">
<FlowRenderer <FlowRenderer
hideBackground={false} hideBackground={false}
hideControls={true} hideControls={true}
@@ -278,7 +251,6 @@ function WorkflowComparisonRenderer({
workflow={version} workflow={version}
/> />
</div> </div>
</div>
); );
} }
@@ -349,8 +321,25 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
<div className="flex h-full w-full flex-col rounded-lg bg-slate-elevation2"> <div className="flex h-full w-full flex-col rounded-lg bg-slate-elevation2">
{/* Header */} {/* Header */}
<div className="flex-shrink-0 p-4 pb-3"> <div className="flex-shrink-0 p-4 pb-3">
<h2 className="mb-2 text-lg font-semibold">Version Comparison</h2> {/* 3x3 Grid Layout */}
<div className="flex gap-3 text-sm"> <div className="grid grid-cols-3 gap-4">
{/* Row 1: Workflow Names and Title */}
<h2 className="text-center text-xl font-semibold">
{version1.title}
</h2>
<h3 className="text-center text-lg font-medium text-muted-foreground">
Version Comparison
</h3>
<h2 className="text-center text-xl font-semibold">
{version2.title}
</h2>
{/* Row 2: Version Details and Statistics */}
<div className="text-center text-sm text-muted-foreground">
[Version {version1.version}] {" "}
{new Date(version1.modified_at).toLocaleDateString()}
</div>
<div className="flex justify-center gap-3 text-sm">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="h-3 w-3 rounded-full bg-green-300"></div> <div className="h-3 w-3 rounded-full bg-green-300"></div>
<span>Identical ({stats.identical})</span> <span>Identical ({stats.identical})</span>
@@ -368,6 +357,36 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
<span>Removed ({stats.removed})</span> <span>Removed ({stats.removed})</span>
</div> </div>
</div> </div>
<div className="text-center text-sm text-muted-foreground">
[Version {version2.version}] {" "}
{new Date(version2.modified_at).toLocaleDateString()}
</div>
{/* Row 3: Select Buttons */}
<div className="flex justify-center">
{onSelectState && (
<Button
size="sm"
onClick={() => onSelectState(version1)}
className="text-xs"
>
Select this variant
</Button>
)}
</div>
<div></div>
<div className="flex justify-center">
{onSelectState && (
<Button
size="sm"
onClick={() => onSelectState(version2)}
className="text-xs"
>
Select this variant
</Button>
)}
</div>
</div>
</div> </div>
<Separator /> <Separator />

View File

@@ -101,6 +101,27 @@ function WorkflowHistoryPanel({ workflowPermanentId, onCompare }: Props) {
</p> </p>
</div> </div>
{/* Compare Buttons */}
<div className="flex-shrink-0 px-4 pb-3">
<div className="flex gap-2">
<Button
variant="secondary"
className="flex-1"
onClick={() => handleCompare("json")}
disabled={!canCompare || isLoading}
>
JSON Diff
</Button>
<Button
className="flex-1"
onClick={() => handleCompare("visual")}
disabled={!canCompare || isLoading}
>
Visual Compare
</Button>
</div>
</div>
<Separator /> <Separator />
{/* Version List */} {/* Version List */}
@@ -166,29 +187,6 @@ function WorkflowHistoryPanel({ workflowPermanentId, onCompare }: Props) {
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
<Separator />
{/* Footer */}
<div className="flex-shrink-0 p-4 pt-3">
<div className="flex gap-2">
<Button
variant="secondary"
className="flex-1"
onClick={() => handleCompare("json")}
disabled={!canCompare || isLoading}
>
JSON Diff
</Button>
<Button
className="flex-1"
onClick={() => handleCompare("visual")}
disabled={!canCompare || isLoading}
>
Visual Compare
</Button>
</div>
</div>
</div> </div>
); );
} }