@dilipod/ui 0.4.15 → 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-viewer.d.ts +14 -0
- package/dist/components/workflow-viewer.d.ts.map +1 -1
- package/dist/index.js +272 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +273 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/workflow-viewer.tsx +367 -60
package/package.json
CHANGED
|
@@ -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')
|
|
@@ -1273,13 +1403,14 @@ export function WorkflowViewer({
|
|
|
1273
1403
|
{pushingToSim ? 'Pushing...' : 'Push to Sim'}
|
|
1274
1404
|
</Button>
|
|
1275
1405
|
)}
|
|
1276
|
-
{
|
|
1277
|
-
<Button
|
|
1278
|
-
onClick={pullFromSim}
|
|
1279
|
-
disabled={pullingFromSim}
|
|
1280
|
-
variant="outline"
|
|
1281
|
-
size="sm"
|
|
1406
|
+
{apiHandlers?.pullFromSim && (
|
|
1407
|
+
<Button
|
|
1408
|
+
onClick={pullFromSim}
|
|
1409
|
+
disabled={pullingFromSim || !simWorkflowId}
|
|
1410
|
+
variant="outline"
|
|
1411
|
+
size="sm"
|
|
1282
1412
|
icon={<CloudArrowDown size={16} />}
|
|
1413
|
+
title={!simWorkflowId ? 'No Sim workflow linked yet. Push to Sim first.' : undefined}
|
|
1283
1414
|
>
|
|
1284
1415
|
{pullingFromSim ? 'Pulling...' : 'Pull from Sim'}
|
|
1285
1416
|
</Button>
|
|
@@ -1362,50 +1493,226 @@ export function WorkflowViewer({
|
|
|
1362
1493
|
) : viewMode === 'backups' ? (
|
|
1363
1494
|
<div className="space-y-3">
|
|
1364
1495
|
<div className="flex items-center justify-between">
|
|
1365
|
-
<h4 className="text-sm font-medium">
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
<div className="py-8 text-center text-muted-foreground">Loading backups...</div>
|
|
1376
|
-
) : backups.length === 0 ? (
|
|
1377
|
-
<div className="py-8 text-center text-muted-foreground">
|
|
1378
|
-
<ClockCounterClockwise size={32} className="mx-auto mb-2 opacity-50" />
|
|
1379
|
-
<p>No backups yet</p>
|
|
1380
|
-
<p className="text-xs mt-1">Click "Export from Sim" to create a backup</p>
|
|
1381
|
-
</div>
|
|
1382
|
-
) : (
|
|
1383
|
-
<div className="space-y-2">
|
|
1384
|
-
{backups.map((backup) => (
|
|
1385
|
-
<div
|
|
1386
|
-
key={backup.id}
|
|
1387
|
-
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} />}
|
|
1388
1506
|
>
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
+
)}
|
|
1403
1651
|
</div>
|
|
1404
|
-
|
|
1652
|
+
)
|
|
1653
|
+
})()
|
|
1654
|
+
) : selectedBackupA || selectedBackupB ? (
|
|
1655
|
+
<div className="py-4 text-center text-muted-foreground text-sm">
|
|
1656
|
+
Select both versions to compare
|
|
1405
1657
|
</div>
|
|
1406
|
-
)
|
|
1658
|
+
) : null}
|
|
1407
1659
|
</div>
|
|
1408
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
|
+
)}
|
|
1409
1716
|
</div>
|
|
1410
1717
|
) : (
|
|
1411
1718
|
platform === 'n8n'
|