@dilipod/ui 0.4.14 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/workflow-flow.d.ts +29 -3
- package/dist/components/workflow-flow.d.ts.map +1 -1
- package/dist/components/workflow-viewer.d.ts +14 -0
- package/dist/components/workflow-viewer.d.ts.map +1 -1
- package/dist/index.js +382 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +383 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/workflow-flow.tsx +163 -5
- package/src/components/workflow-viewer.tsx +381 -78
|
@@ -7,25 +7,30 @@ import { Badge } from './badge'
|
|
|
7
7
|
import { Textarea } from './textarea'
|
|
8
8
|
import { Alert } from './alert'
|
|
9
9
|
import { Select } from './select'
|
|
10
|
-
import {
|
|
11
|
-
WebhooksLogo,
|
|
12
|
-
Timer,
|
|
13
|
-
GitBranch,
|
|
14
|
-
Globe,
|
|
15
|
-
Robot,
|
|
16
|
-
Code,
|
|
17
|
-
CheckCircle,
|
|
18
|
-
Package,
|
|
19
|
-
CloudArrowUp,
|
|
20
|
-
CloudArrowDown,
|
|
21
|
-
Copy,
|
|
22
|
-
PencilSimple,
|
|
23
|
-
Eye,
|
|
10
|
+
import {
|
|
11
|
+
WebhooksLogo,
|
|
12
|
+
Timer,
|
|
13
|
+
GitBranch,
|
|
14
|
+
Globe,
|
|
15
|
+
Robot,
|
|
16
|
+
Code,
|
|
17
|
+
CheckCircle,
|
|
18
|
+
Package,
|
|
19
|
+
CloudArrowUp,
|
|
20
|
+
CloudArrowDown,
|
|
21
|
+
Copy,
|
|
22
|
+
PencilSimple,
|
|
23
|
+
Eye,
|
|
24
24
|
TreeStructure,
|
|
25
25
|
DownloadSimple,
|
|
26
26
|
ArrowSquareOut,
|
|
27
27
|
ClockCounterClockwise,
|
|
28
28
|
ArrowsClockwise,
|
|
29
|
+
ArrowsLeftRight,
|
|
30
|
+
Plus,
|
|
31
|
+
Minus,
|
|
32
|
+
Pencil,
|
|
33
|
+
X,
|
|
29
34
|
} from '@phosphor-icons/react'
|
|
30
35
|
|
|
31
36
|
// Lazy load the flow visualization to avoid SSR issues
|
|
@@ -151,6 +156,8 @@ export interface WorkflowViewerProps {
|
|
|
151
156
|
exportFromSim?: (workflowDefId: string) => Promise<{ success: boolean; error?: string; backup?: { id: string; version: number; exportedAt: string }; workflow?: { blocksCount: number; edgesCount: number } }>
|
|
152
157
|
/** Get backup history for Sim workflow */
|
|
153
158
|
getSimBackups?: (workflowDefId: string) => Promise<{ success: boolean; error?: string; backups?: Array<{ id: string; version: number; versionLabel?: string; workflowName: string; isDeployed: boolean; exportedAt: string }> }>
|
|
159
|
+
/** Get a specific backup's full state */
|
|
160
|
+
getBackupState?: (workflowDefId: string, backupId: string) => Promise<{ success: boolean; error?: string; backup?: { id: string; version: number; versionLabel?: string; workflowName: string; isDeployed: boolean; exportedAt: string; state: SimWorkflow | null } }>
|
|
154
161
|
/** Push workflow to Sim Studio */
|
|
155
162
|
pushToSim?: (workflowDefId: string) => Promise<{ success: boolean; error?: string; workflowId?: string }>
|
|
156
163
|
/** Pull workflow from Sim Studio */
|
|
@@ -261,6 +268,82 @@ function defaultFormatDistance(date: Date, options?: { addSuffix?: boolean }): s
|
|
|
261
268
|
return options?.addSuffix ? `${result} ago` : result
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
// ============================================
|
|
272
|
+
// Diff Utilities
|
|
273
|
+
// ============================================
|
|
274
|
+
|
|
275
|
+
interface DiffResult {
|
|
276
|
+
blocksAdded: string[]
|
|
277
|
+
blocksRemoved: string[]
|
|
278
|
+
blocksModified: string[]
|
|
279
|
+
edgesAdded: number
|
|
280
|
+
edgesRemoved: number
|
|
281
|
+
summary: string
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function computeWorkflowDiff(stateA: SimWorkflow | null, stateB: SimWorkflow | null): DiffResult {
|
|
285
|
+
if (!stateA || !stateB) {
|
|
286
|
+
return {
|
|
287
|
+
blocksAdded: [],
|
|
288
|
+
blocksRemoved: [],
|
|
289
|
+
blocksModified: [],
|
|
290
|
+
edgesAdded: 0,
|
|
291
|
+
edgesRemoved: 0,
|
|
292
|
+
summary: 'Unable to compare - missing workflow data',
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const blocksA = stateA.blocks || {}
|
|
297
|
+
const blocksB = stateB.blocks || {}
|
|
298
|
+
const edgesA = stateA.edges || []
|
|
299
|
+
const edgesB = stateB.edges || []
|
|
300
|
+
|
|
301
|
+
const blockIdsA = new Set(Object.keys(blocksA))
|
|
302
|
+
const blockIdsB = new Set(Object.keys(blocksB))
|
|
303
|
+
|
|
304
|
+
const blocksAdded = [...blockIdsB].filter(id => !blockIdsA.has(id))
|
|
305
|
+
const blocksRemoved = [...blockIdsA].filter(id => !blockIdsB.has(id))
|
|
306
|
+
|
|
307
|
+
// Check for modified blocks (blocks that exist in both but have different content)
|
|
308
|
+
const blocksModified: string[] = []
|
|
309
|
+
for (const id of blockIdsA) {
|
|
310
|
+
if (blockIdsB.has(id)) {
|
|
311
|
+
const blockA = blocksA[id]
|
|
312
|
+
const blockB = blocksB[id]
|
|
313
|
+
// Deep compare by stringifying
|
|
314
|
+
if (JSON.stringify(blockA) !== JSON.stringify(blockB)) {
|
|
315
|
+
blocksModified.push(id)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Compare edges by creating a signature for each
|
|
321
|
+
const edgeSignature = (e: { source?: string; target?: string; from?: string; to?: string }) =>
|
|
322
|
+
`${e.source || e.from}->${e.target || e.to}`
|
|
323
|
+
const edgeSigsA = new Set(edgesA.map(edgeSignature))
|
|
324
|
+
const edgeSigsB = new Set(edgesB.map(edgeSignature))
|
|
325
|
+
|
|
326
|
+
const edgesAdded = [...edgeSigsB].filter(sig => !edgeSigsA.has(sig)).length
|
|
327
|
+
const edgesRemoved = [...edgeSigsA].filter(sig => !edgeSigsB.has(sig)).length
|
|
328
|
+
|
|
329
|
+
// Generate summary
|
|
330
|
+
const changes: string[] = []
|
|
331
|
+
if (blocksAdded.length > 0) changes.push(`+${blocksAdded.length} block${blocksAdded.length > 1 ? 's' : ''}`)
|
|
332
|
+
if (blocksRemoved.length > 0) changes.push(`-${blocksRemoved.length} block${blocksRemoved.length > 1 ? 's' : ''}`)
|
|
333
|
+
if (blocksModified.length > 0) changes.push(`~${blocksModified.length} modified`)
|
|
334
|
+
if (edgesAdded > 0) changes.push(`+${edgesAdded} edge${edgesAdded > 1 ? 's' : ''}`)
|
|
335
|
+
if (edgesRemoved > 0) changes.push(`-${edgesRemoved} edge${edgesRemoved > 1 ? 's' : ''}`)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
blocksAdded,
|
|
339
|
+
blocksRemoved,
|
|
340
|
+
blocksModified,
|
|
341
|
+
edgesAdded,
|
|
342
|
+
edgesRemoved,
|
|
343
|
+
summary: changes.length > 0 ? changes.join(', ') : 'No changes detected',
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
264
347
|
// ============================================
|
|
265
348
|
// N8n Workflow Summary Component
|
|
266
349
|
// ============================================
|
|
@@ -542,7 +625,14 @@ export function WorkflowViewer({
|
|
|
542
625
|
const [pushingToSim, setPushingToSim] = useState(false)
|
|
543
626
|
const [pullingFromSim, setPullingFromSim] = useState(false)
|
|
544
627
|
const [switchingPlatform, setSwitchingPlatform] = useState(false)
|
|
545
|
-
|
|
628
|
+
// Diff comparison state
|
|
629
|
+
const [diffMode, setDiffMode] = useState(false)
|
|
630
|
+
const [selectedBackupA, setSelectedBackupA] = useState<string | null>(null)
|
|
631
|
+
const [selectedBackupB, setSelectedBackupB] = useState<string | null>(null)
|
|
632
|
+
const [backupStateA, setBackupStateA] = useState<SimWorkflow | null>(null)
|
|
633
|
+
const [backupStateB, setBackupStateB] = useState<SimWorkflow | null>(null)
|
|
634
|
+
const [loadingDiff, setLoadingDiff] = useState(false)
|
|
635
|
+
|
|
546
636
|
const [localPlatform, setLocalPlatform] = useState<'n8n' | 'sim'>(platform)
|
|
547
637
|
const [localIsActive, setLocalIsActive] = useState(isActive ?? true)
|
|
548
638
|
|
|
@@ -784,6 +874,46 @@ export function WorkflowViewer({
|
|
|
784
874
|
loadBackups()
|
|
785
875
|
}
|
|
786
876
|
|
|
877
|
+
async function loadBackupForDiff(backupId: string, slot: 'A' | 'B') {
|
|
878
|
+
if (!workflowDefinitionId || !apiHandlers?.getBackupState) {
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
setLoadingDiff(true)
|
|
883
|
+
try {
|
|
884
|
+
const result = await apiHandlers.getBackupState(workflowDefinitionId, backupId)
|
|
885
|
+
if (result.success && result.backup) {
|
|
886
|
+
if (slot === 'A') {
|
|
887
|
+
setBackupStateA(result.backup.state)
|
|
888
|
+
setSelectedBackupA(backupId)
|
|
889
|
+
} else {
|
|
890
|
+
setBackupStateB(result.backup.state)
|
|
891
|
+
setSelectedBackupB(backupId)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} catch {
|
|
895
|
+
console.error('Failed to load backup for diff')
|
|
896
|
+
} finally {
|
|
897
|
+
setLoadingDiff(false)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function startDiffMode() {
|
|
902
|
+
setDiffMode(true)
|
|
903
|
+
setSelectedBackupA(null)
|
|
904
|
+
setSelectedBackupB(null)
|
|
905
|
+
setBackupStateA(null)
|
|
906
|
+
setBackupStateB(null)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function exitDiffMode() {
|
|
910
|
+
setDiffMode(false)
|
|
911
|
+
setSelectedBackupA(null)
|
|
912
|
+
setSelectedBackupB(null)
|
|
913
|
+
setBackupStateA(null)
|
|
914
|
+
setBackupStateB(null)
|
|
915
|
+
}
|
|
916
|
+
|
|
787
917
|
function openInSimStudio() {
|
|
788
918
|
if (simStudioUrl && simWorkflowId) {
|
|
789
919
|
window.open(`${simStudioUrl}/w/${simWorkflowId}`, '_blank')
|
|
@@ -1116,19 +1246,17 @@ export function WorkflowViewer({
|
|
|
1116
1246
|
<Eye size={14} />
|
|
1117
1247
|
Summary
|
|
1118
1248
|
</button>
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
</button>
|
|
1131
|
-
)}
|
|
1249
|
+
<button
|
|
1250
|
+
onClick={() => setViewMode('flow')}
|
|
1251
|
+
className={`px-3 py-1.5 text-xs font-medium flex items-center gap-1.5 transition-colors border-l border-border ${
|
|
1252
|
+
viewMode === 'flow'
|
|
1253
|
+
? 'bg-primary text-white'
|
|
1254
|
+
: 'bg-background hover:bg-muted text-muted-foreground'
|
|
1255
|
+
}`}
|
|
1256
|
+
>
|
|
1257
|
+
<TreeStructure size={14} />
|
|
1258
|
+
Diagram
|
|
1259
|
+
</button>
|
|
1132
1260
|
<button
|
|
1133
1261
|
onClick={() => setViewMode('json')}
|
|
1134
1262
|
className={`px-3 py-1.5 text-xs font-medium flex items-center gap-1.5 transition-colors border-l border-border ${
|
|
@@ -1275,13 +1403,14 @@ export function WorkflowViewer({
|
|
|
1275
1403
|
{pushingToSim ? 'Pushing...' : 'Push to Sim'}
|
|
1276
1404
|
</Button>
|
|
1277
1405
|
)}
|
|
1278
|
-
{
|
|
1279
|
-
<Button
|
|
1280
|
-
onClick={pullFromSim}
|
|
1281
|
-
disabled={pullingFromSim}
|
|
1282
|
-
variant="outline"
|
|
1283
|
-
size="sm"
|
|
1406
|
+
{apiHandlers?.pullFromSim && (
|
|
1407
|
+
<Button
|
|
1408
|
+
onClick={pullFromSim}
|
|
1409
|
+
disabled={pullingFromSim || !simWorkflowId}
|
|
1410
|
+
variant="outline"
|
|
1411
|
+
size="sm"
|
|
1284
1412
|
icon={<CloudArrowDown size={16} />}
|
|
1413
|
+
title={!simWorkflowId ? 'No Sim workflow linked yet. Push to Sim first.' : undefined}
|
|
1285
1414
|
>
|
|
1286
1415
|
{pullingFromSim ? 'Pulling...' : 'Pull from Sim'}
|
|
1287
1416
|
</Button>
|
|
@@ -1358,58 +1487,232 @@ export function WorkflowViewer({
|
|
|
1358
1487
|
</pre>
|
|
1359
1488
|
</div>
|
|
1360
1489
|
) : viewMode === 'flow' ? (
|
|
1361
|
-
|
|
1362
|
-
<
|
|
1363
|
-
|
|
1364
|
-
</Suspense>
|
|
1365
|
-
)
|
|
1490
|
+
<Suspense fallback={loadingComponent || DefaultLoading}>
|
|
1491
|
+
<WorkflowFlow workflow={workflow as any} height={380} platform={platform} />
|
|
1492
|
+
</Suspense>
|
|
1366
1493
|
) : viewMode === 'backups' ? (
|
|
1367
1494
|
<div className="space-y-3">
|
|
1368
1495
|
<div className="flex items-center justify-between">
|
|
1369
|
-
<h4 className="text-sm font-medium">
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
<div className="py-8 text-center text-muted-foreground">Loading backups...</div>
|
|
1380
|
-
) : backups.length === 0 ? (
|
|
1381
|
-
<div className="py-8 text-center text-muted-foreground">
|
|
1382
|
-
<ClockCounterClockwise size={32} className="mx-auto mb-2 opacity-50" />
|
|
1383
|
-
<p>No backups yet</p>
|
|
1384
|
-
<p className="text-xs mt-1">Click "Export from Sim" to create a backup</p>
|
|
1385
|
-
</div>
|
|
1386
|
-
) : (
|
|
1387
|
-
<div className="space-y-2">
|
|
1388
|
-
{backups.map((backup) => (
|
|
1389
|
-
<div
|
|
1390
|
-
key={backup.id}
|
|
1391
|
-
className="flex items-center justify-between p-3 bg-muted/50 rounded border border-border"
|
|
1496
|
+
<h4 className="text-sm font-medium">
|
|
1497
|
+
{diffMode ? 'Compare Versions' : 'Backup History'}
|
|
1498
|
+
</h4>
|
|
1499
|
+
<div className="flex items-center gap-2">
|
|
1500
|
+
{!diffMode && backups.length >= 2 && apiHandlers?.getBackupState && (
|
|
1501
|
+
<Button
|
|
1502
|
+
onClick={startDiffMode}
|
|
1503
|
+
variant="outline"
|
|
1504
|
+
size="sm"
|
|
1505
|
+
icon={<ArrowsLeftRight size={14} />}
|
|
1392
1506
|
>
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1507
|
+
Compare
|
|
1508
|
+
</Button>
|
|
1509
|
+
)}
|
|
1510
|
+
{diffMode && (
|
|
1511
|
+
<Button
|
|
1512
|
+
onClick={exitDiffMode}
|
|
1513
|
+
variant="outline"
|
|
1514
|
+
size="sm"
|
|
1515
|
+
icon={<X size={14} />}
|
|
1516
|
+
>
|
|
1517
|
+
Cancel
|
|
1518
|
+
</Button>
|
|
1519
|
+
)}
|
|
1520
|
+
<Button
|
|
1521
|
+
onClick={() => { exitDiffMode(); setViewMode('summary'); }}
|
|
1522
|
+
variant="outline"
|
|
1523
|
+
size="sm"
|
|
1524
|
+
>
|
|
1525
|
+
Back to Summary
|
|
1526
|
+
</Button>
|
|
1527
|
+
</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
|
|
1530
|
+
{/* Diff mode: version selection and results */}
|
|
1531
|
+
{diffMode && (
|
|
1532
|
+
<div className="space-y-3">
|
|
1533
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1534
|
+
<div className="p-3 bg-muted/50 rounded border border-border">
|
|
1535
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">From (older)</p>
|
|
1536
|
+
<Select
|
|
1537
|
+
value={selectedBackupA || ''}
|
|
1538
|
+
onChange={(e) => loadBackupForDiff(e.target.value, 'A')}
|
|
1539
|
+
disabled={loadingDiff}
|
|
1540
|
+
className="w-full"
|
|
1541
|
+
>
|
|
1542
|
+
<option value="">Select version...</option>
|
|
1543
|
+
{backups.map((b) => (
|
|
1544
|
+
<option key={b.id} value={b.id} disabled={b.id === selectedBackupB}>
|
|
1545
|
+
v{b.version} - {b.versionLabel || b.workflowName}
|
|
1546
|
+
</option>
|
|
1547
|
+
))}
|
|
1548
|
+
</Select>
|
|
1549
|
+
</div>
|
|
1550
|
+
<div className="p-3 bg-muted/50 rounded border border-border">
|
|
1551
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">To (newer)</p>
|
|
1552
|
+
<Select
|
|
1553
|
+
value={selectedBackupB || ''}
|
|
1554
|
+
onChange={(e) => loadBackupForDiff(e.target.value, 'B')}
|
|
1555
|
+
disabled={loadingDiff}
|
|
1556
|
+
className="w-full"
|
|
1557
|
+
>
|
|
1558
|
+
<option value="">Select version...</option>
|
|
1559
|
+
{backups.map((b) => (
|
|
1560
|
+
<option key={b.id} value={b.id} disabled={b.id === selectedBackupA}>
|
|
1561
|
+
v{b.version} - {b.versionLabel || b.workflowName}
|
|
1562
|
+
</option>
|
|
1563
|
+
))}
|
|
1564
|
+
</Select>
|
|
1565
|
+
</div>
|
|
1566
|
+
</div>
|
|
1567
|
+
|
|
1568
|
+
{/* Diff results */}
|
|
1569
|
+
{loadingDiff ? (
|
|
1570
|
+
<div className="py-4 text-center text-muted-foreground">Loading versions...</div>
|
|
1571
|
+
) : backupStateA && backupStateB ? (
|
|
1572
|
+
(() => {
|
|
1573
|
+
const diff = computeWorkflowDiff(backupStateA, backupStateB)
|
|
1574
|
+
return (
|
|
1575
|
+
<div className="space-y-3">
|
|
1576
|
+
<div className="p-3 bg-muted/30 rounded border border-border">
|
|
1577
|
+
<p className="text-sm font-medium mb-2">Changes Summary</p>
|
|
1578
|
+
<p className="text-sm text-muted-foreground">{diff.summary}</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
|
|
1581
|
+
{diff.blocksAdded.length > 0 && (
|
|
1582
|
+
<div className="p-3 bg-green-500/10 rounded border border-green-500/30">
|
|
1583
|
+
<div className="flex items-center gap-2 text-green-600 dark:text-green-400 mb-2">
|
|
1584
|
+
<Plus size={14} />
|
|
1585
|
+
<span className="text-xs font-medium">Blocks Added ({diff.blocksAdded.length})</span>
|
|
1586
|
+
</div>
|
|
1587
|
+
<div className="flex flex-wrap gap-1">
|
|
1588
|
+
{diff.blocksAdded.map((id) => {
|
|
1589
|
+
const block = backupStateB?.blocks?.[id]
|
|
1590
|
+
return (
|
|
1591
|
+
<Badge key={id} variant="success" size="sm">
|
|
1592
|
+
{block?.name || id}
|
|
1593
|
+
</Badge>
|
|
1594
|
+
)
|
|
1595
|
+
})}
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
)}
|
|
1599
|
+
|
|
1600
|
+
{diff.blocksRemoved.length > 0 && (
|
|
1601
|
+
<div className="p-3 bg-red-500/10 rounded border border-red-500/30">
|
|
1602
|
+
<div className="flex items-center gap-2 text-red-600 dark:text-red-400 mb-2">
|
|
1603
|
+
<Minus size={14} />
|
|
1604
|
+
<span className="text-xs font-medium">Blocks Removed ({diff.blocksRemoved.length})</span>
|
|
1605
|
+
</div>
|
|
1606
|
+
<div className="flex flex-wrap gap-1">
|
|
1607
|
+
{diff.blocksRemoved.map((id) => {
|
|
1608
|
+
const block = backupStateA?.blocks?.[id]
|
|
1609
|
+
return (
|
|
1610
|
+
<Badge key={id} variant="error" size="sm">
|
|
1611
|
+
{block?.name || id}
|
|
1612
|
+
</Badge>
|
|
1613
|
+
)
|
|
1614
|
+
})}
|
|
1615
|
+
</div>
|
|
1616
|
+
</div>
|
|
1617
|
+
)}
|
|
1618
|
+
|
|
1619
|
+
{diff.blocksModified.length > 0 && (
|
|
1620
|
+
<div className="p-3 bg-amber-500/10 rounded border border-amber-500/30">
|
|
1621
|
+
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400 mb-2">
|
|
1622
|
+
<Pencil size={14} />
|
|
1623
|
+
<span className="text-xs font-medium">Blocks Modified ({diff.blocksModified.length})</span>
|
|
1624
|
+
</div>
|
|
1625
|
+
<div className="flex flex-wrap gap-1">
|
|
1626
|
+
{diff.blocksModified.map((id) => {
|
|
1627
|
+
const block = backupStateB?.blocks?.[id]
|
|
1628
|
+
return (
|
|
1629
|
+
<Badge key={id} variant="warning" size="sm">
|
|
1630
|
+
{block?.name || id}
|
|
1631
|
+
</Badge>
|
|
1632
|
+
)
|
|
1633
|
+
})}
|
|
1634
|
+
</div>
|
|
1635
|
+
</div>
|
|
1636
|
+
)}
|
|
1637
|
+
|
|
1638
|
+
{(diff.edgesAdded > 0 || diff.edgesRemoved > 0) && (
|
|
1639
|
+
<div className="p-3 bg-muted/30 rounded border border-border">
|
|
1640
|
+
<p className="text-xs font-medium text-muted-foreground mb-1">Connection Changes</p>
|
|
1641
|
+
<div className="flex gap-3 text-sm">
|
|
1642
|
+
{diff.edgesAdded > 0 && (
|
|
1643
|
+
<span className="text-green-600 dark:text-green-400">+{diff.edgesAdded} added</span>
|
|
1644
|
+
)}
|
|
1645
|
+
{diff.edgesRemoved > 0 && (
|
|
1646
|
+
<span className="text-red-600 dark:text-red-400">-{diff.edgesRemoved} removed</span>
|
|
1647
|
+
)}
|
|
1648
|
+
</div>
|
|
1649
|
+
</div>
|
|
1650
|
+
)}
|
|
1407
1651
|
</div>
|
|
1408
|
-
|
|
1652
|
+
)
|
|
1653
|
+
})()
|
|
1654
|
+
) : selectedBackupA || selectedBackupB ? (
|
|
1655
|
+
<div className="py-4 text-center text-muted-foreground text-sm">
|
|
1656
|
+
Select both versions to compare
|
|
1409
1657
|
</div>
|
|
1410
|
-
)
|
|
1658
|
+
) : null}
|
|
1411
1659
|
</div>
|
|
1412
1660
|
)}
|
|
1661
|
+
|
|
1662
|
+
{/* Regular backup list (when not in diff mode) */}
|
|
1663
|
+
{!diffMode && (
|
|
1664
|
+
<>
|
|
1665
|
+
{loadingBackups ? (
|
|
1666
|
+
<div className="py-8 text-center text-muted-foreground">Loading backups...</div>
|
|
1667
|
+
) : backups.length === 0 ? (
|
|
1668
|
+
<div className="py-8 text-center text-muted-foreground">
|
|
1669
|
+
<ClockCounterClockwise size={32} className="mx-auto mb-2 opacity-50" />
|
|
1670
|
+
<p>No backups yet</p>
|
|
1671
|
+
<p className="text-xs mt-1">Click "Export from Sim" to create a backup</p>
|
|
1672
|
+
</div>
|
|
1673
|
+
) : (
|
|
1674
|
+
<div className="space-y-2">
|
|
1675
|
+
{backups.map((backup, index) => (
|
|
1676
|
+
<div
|
|
1677
|
+
key={backup.id}
|
|
1678
|
+
className="flex items-center justify-between p-3 bg-muted/50 rounded border border-border"
|
|
1679
|
+
>
|
|
1680
|
+
<div className="flex items-center gap-3">
|
|
1681
|
+
<div className="flex items-center justify-center w-8 h-8 rounded bg-primary/10 text-primary text-sm font-semibold">
|
|
1682
|
+
v{backup.version}
|
|
1683
|
+
</div>
|
|
1684
|
+
<div>
|
|
1685
|
+
<p className="text-sm font-medium">
|
|
1686
|
+
{backup.versionLabel || backup.workflowName}
|
|
1687
|
+
</p>
|
|
1688
|
+
<p className="text-xs text-muted-foreground">
|
|
1689
|
+
{formatDistance(new Date(backup.exportedAt), { addSuffix: true })}
|
|
1690
|
+
{backup.isDeployed && (
|
|
1691
|
+
<Badge variant="success" size="sm" className="ml-2">Deployed</Badge>
|
|
1692
|
+
)}
|
|
1693
|
+
</p>
|
|
1694
|
+
</div>
|
|
1695
|
+
</div>
|
|
1696
|
+
{index < backups.length - 1 && apiHandlers?.getBackupState && (
|
|
1697
|
+
<Button
|
|
1698
|
+
variant="ghost"
|
|
1699
|
+
size="sm"
|
|
1700
|
+
onClick={() => {
|
|
1701
|
+
startDiffMode()
|
|
1702
|
+
loadBackupForDiff(backups[index + 1].id, 'A')
|
|
1703
|
+
loadBackupForDiff(backup.id, 'B')
|
|
1704
|
+
}}
|
|
1705
|
+
icon={<ArrowsLeftRight size={14} />}
|
|
1706
|
+
>
|
|
1707
|
+
Diff
|
|
1708
|
+
</Button>
|
|
1709
|
+
)}
|
|
1710
|
+
</div>
|
|
1711
|
+
))}
|
|
1712
|
+
</div>
|
|
1713
|
+
)}
|
|
1714
|
+
</>
|
|
1715
|
+
)}
|
|
1413
1716
|
</div>
|
|
1414
1717
|
) : (
|
|
1415
1718
|
platform === 'n8n'
|