Feature: workflow history, buttons moved on top (#3492)
This commit is contained in:
@@ -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,39 +915,26 @@ 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 && (
|
{workflowPanelState.data?.showComparison &&
|
||||||
|
workflowPanelState.data?.version1 &&
|
||||||
|
workflowPanelState.data?.version2 ? (
|
||||||
<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 or comparison view */}
|
{/* comparison view */}
|
||||||
{workflowPanelState.data?.showComparison &&
|
<div
|
||||||
workflowPanelState.data?.version1 &&
|
className="absolute left-6 top-[6rem]"
|
||||||
workflowPanelState.data?.version2 ? (
|
style={{
|
||||||
<div
|
width: "calc(100% - 32rem)",
|
||||||
className="absolute left-6 top-[6rem]"
|
height: "calc(100vh - 11rem)",
|
||||||
style={{
|
}}
|
||||||
width: "calc(100% - 32rem)",
|
>
|
||||||
height: "calc(100vh - 11rem)",
|
<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}
|
||||||
<WorkflowComparisonPanel
|
version2={workflowPanelState.data.version2}
|
||||||
key={`${workflowPanelState.data.version1.workflow_id}v${workflowPanelState.data.version1.version}-${workflowPanelState.data.version2.workflow_id}v${workflowPanelState.data.version2.version}`}
|
onSelectState={handleSelectState}
|
||||||
version1={workflowPanelState.data.version1}
|
|
||||||
version2={workflowPanelState.data.version2}
|
|
||||||
onSelectState={handleSelectState}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FlowRenderer
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
setNodes={setNodes}
|
|
||||||
setEdges={setEdges}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
initialTitle={initialTitle}
|
|
||||||
workflow={workflow}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* sub panels */}
|
{/* sub panels */}
|
||||||
{workflowPanelState.active && (
|
{workflowPanelState.active && (
|
||||||
@@ -984,46 +991,122 @@ function Workspace({
|
|||||||
</div>
|
</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
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
setNodes={setNodes}
|
||||||
|
setEdges={setEdges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
initialTitle={initialTitle}
|
||||||
|
workflow={workflow}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* sub panels when in debug mode */}
|
{/* sub panels when in debug mode */}
|
||||||
{showBrowser && workflowPanelState.active && (
|
{showBrowser &&
|
||||||
<div
|
!workflowPanelState.data?.showComparison &&
|
||||||
className="absolute right-6 top-[8.5rem] z-30"
|
workflowPanelState.active && (
|
||||||
style={{
|
<div
|
||||||
height:
|
className="absolute right-6 top-[8.5rem] z-30"
|
||||||
workflowPanelState.content === "nodeLibrary"
|
style={{
|
||||||
? "calc(100vh - 14rem)"
|
height:
|
||||||
: "unset",
|
workflowPanelState.content === "nodeLibrary"
|
||||||
}}
|
? "calc(100vh - 14rem)"
|
||||||
>
|
: "unset",
|
||||||
{workflowPanelState.content === "cacheKeyValues" && (
|
}}
|
||||||
<WorkflowCacheKeyValuesPanel
|
>
|
||||||
cacheKeyValues={cacheKeyValues}
|
{workflowPanelState.content === "cacheKeyValues" && (
|
||||||
pending={cacheKeyValuesLoading}
|
<WorkflowCacheKeyValuesPanel
|
||||||
scriptKey={workflow.cache_key ?? "default"}
|
cacheKeyValues={cacheKeyValues}
|
||||||
onDelete={(cacheKeyValue) => {
|
pending={cacheKeyValuesLoading}
|
||||||
setToDeleteCacheKeyValue(cacheKeyValue);
|
scriptKey={workflow.cache_key ?? "default"}
|
||||||
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
onDelete={(cacheKeyValue) => {
|
||||||
}}
|
setToDeleteCacheKeyValue(cacheKeyValue);
|
||||||
onPaginate={(page) => {
|
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
||||||
setPage(page);
|
}}
|
||||||
}}
|
onPaginate={(page) => {
|
||||||
onSelect={(cacheKeyValue) => {
|
setPage(page);
|
||||||
setCacheKeyValue(cacheKeyValue);
|
}}
|
||||||
setCacheKeyValueFilter("");
|
onSelect={(cacheKeyValue) => {
|
||||||
closeWorkflowPanel();
|
setCacheKeyValue(cacheKeyValue);
|
||||||
}}
|
setCacheKeyValueFilter("");
|
||||||
/>
|
closeWorkflowPanel();
|
||||||
)}
|
}}
|
||||||
{workflowPanelState.content === "parameters" && (
|
/>
|
||||||
<WorkflowParametersPanel />
|
)}
|
||||||
)}
|
{workflowPanelState.content === "parameters" && (
|
||||||
</div>
|
<WorkflowParametersPanel />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 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,82 +1115,64 @@ 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 &&
|
<div
|
||||||
workflowPanelState.data?.version1 &&
|
className={cn(
|
||||||
workflowPanelState.data?.version2 ? (
|
"skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300",
|
||||||
|
{
|
||||||
|
"w-[100%] translate-x-0":
|
||||||
|
leftSideLayoutMode === "side-by-side",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"translate-x-0": showAllCode,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
ref={dom.splitLeft}
|
||||||
|
>
|
||||||
|
{/* code */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 top-[8.5rem] p-6"
|
className={cn("h-full w-[50%]", {
|
||||||
style={{
|
"w-[0%]":
|
||||||
height: "calc(100vh - 14.5rem)",
|
leftSideLayoutMode === "side-by-side" && !showAllCode,
|
||||||
}}
|
})}
|
||||||
>
|
>
|
||||||
<WorkflowComparisonPanel
|
<div className="relative mt-[8.5rem] w-full p-6 pr-5 pt-0">
|
||||||
key={`${workflowPanelState.data.version1.workflow_id}v${workflowPanelState.data.version1.version}-${workflowPanelState.data.version2.workflow_id}v${workflowPanelState.data.version2.version}`}
|
<div className="absolute right-[1.25rem] top-0 z-20">
|
||||||
version1={workflowPanelState.data.version1}
|
<CopyText text={code} />
|
||||||
version2={workflowPanelState.data.version2}
|
|
||||||
onSelectState={handleSelectState}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"skyvern-split-left flex h-full w-[200%] translate-x-[-50%] transition-none duration-300",
|
|
||||||
{
|
|
||||||
"w-[100%] translate-x-0":
|
|
||||||
leftSideLayoutMode === "side-by-side",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"translate-x-0": showAllCode,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
ref={dom.splitLeft}
|
|
||||||
>
|
|
||||||
{/* code */}
|
|
||||||
<div
|
|
||||||
className={cn("h-full w-[50%]", {
|
|
||||||
"w-[0%]":
|
|
||||||
leftSideLayoutMode === "side-by-side" && !showAllCode,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="relative mt-[8.5rem] w-full p-6 pr-5 pt-0">
|
|
||||||
<div className="absolute right-[1.25rem] top-0 z-20">
|
|
||||||
<CopyText text={code} />
|
|
||||||
</div>
|
|
||||||
<CodeEditor
|
|
||||||
className="w-full overflow-y-scroll"
|
|
||||||
language="python"
|
|
||||||
value={code}
|
|
||||||
lineWrap={false}
|
|
||||||
readOnly
|
|
||||||
fontSize={10}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<CodeEditor
|
||||||
{/* infinite canvas */}
|
className="w-full overflow-y-scroll"
|
||||||
<div
|
language="python"
|
||||||
className={cn("h-full w-[50%]", {
|
value={code}
|
||||||
"w-[100%]":
|
lineWrap={false}
|
||||||
leftSideLayoutMode === "side-by-side" && !showAllCode,
|
readOnly
|
||||||
})}
|
fontSize={10}
|
||||||
>
|
|
||||||
<FlowRenderer
|
|
||||||
hideBackground={true}
|
|
||||||
hideControls={true}
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
setNodes={setNodes}
|
|
||||||
setEdges={setEdges}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
initialTitle={initialTitle}
|
|
||||||
workflow={workflow}
|
|
||||||
onContainerResize={containerResizeTrigger}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{/* infinite canvas */}
|
||||||
|
<div
|
||||||
|
className={cn("h-full w-[50%]", {
|
||||||
|
"w-[100%]":
|
||||||
|
leftSideLayoutMode === "side-by-side" && !showAllCode,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FlowRenderer
|
||||||
|
hideBackground={true}
|
||||||
|
hideControls={true}
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
setNodes={setNodes}
|
||||||
|
setEdges={setEdges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
initialTitle={initialTitle}
|
||||||
|
workflow={workflow}
|
||||||
|
onContainerResize={containerResizeTrigger}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* browser & timeline */}
|
{/* browser & timeline */}
|
||||||
|
|||||||
@@ -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,45 +237,19 @@ 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">
|
<FlowRenderer
|
||||||
<div className="text-center">
|
hideBackground={false}
|
||||||
<div className="mb-1 flex items-center justify-center gap-2">
|
hideControls={true}
|
||||||
<Badge variant="secondary">
|
nodes={nodes}
|
||||||
{version.title}, version: {version.version}
|
edges={edges}
|
||||||
</Badge>
|
setNodes={setNodes}
|
||||||
<Badge variant="secondary">
|
setEdges={setEdges}
|
||||||
{version.workflow_definition?.blocks?.length || 0} block
|
onNodesChange={handleNodesChange}
|
||||||
{(version.workflow_definition?.blocks?.length || 0) !== 1
|
onEdgesChange={handleEdgesChange}
|
||||||
? "s"
|
initialTitle={version.title}
|
||||||
: ""}
|
workflow={version}
|
||||||
</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
|
|
||||||
hideBackground={false}
|
|
||||||
hideControls={true}
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
setNodes={setNodes}
|
|
||||||
setEdges={setEdges}
|
|
||||||
onNodesChange={handleNodesChange}
|
|
||||||
onEdgesChange={handleEdgesChange}
|
|
||||||
initialTitle={version.title}
|
|
||||||
workflow={version}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -349,23 +321,70 @@ 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">
|
||||||
<div className="flex items-center gap-1">
|
{/* Row 1: Workflow Names and Title */}
|
||||||
<div className="h-3 w-3 rounded-full bg-green-300"></div>
|
<h2 className="text-center text-xl font-semibold">
|
||||||
<span>Identical ({stats.identical})</span>
|
{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>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex justify-center gap-3 text-sm">
|
||||||
<div className="h-3 w-3 rounded-full bg-yellow-400"></div>
|
<div className="flex items-center gap-1">
|
||||||
<span>Modified ({stats.modified})</span>
|
<div className="h-3 w-3 rounded-full bg-green-300"></div>
|
||||||
|
<span>Identical ({stats.identical})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-yellow-400"></div>
|
||||||
|
<span>Modified ({stats.modified})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-orange-700"></div>
|
||||||
|
<span>Added ({stats.added})</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-orange-700"></div>
|
||||||
|
<span>Removed ({stats.removed})</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
<div className="h-3 w-3 rounded-full bg-orange-700"></div>
|
[Version {version2.version}] •{" "}
|
||||||
<span>Added ({stats.added})</span>
|
{new Date(version2.modified_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="h-3 w-3 rounded-full bg-orange-700"></div>
|
{/* Row 3: Select Buttons */}
|
||||||
<span>Removed ({stats.removed})</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user