@gadmin2n/schematics 0.0.78 → 0.0.80

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.
@@ -9,7 +9,6 @@ import {
9
9
  Spin,
10
10
  Steps,
11
11
  Tag,
12
- Tooltip,
13
12
  Typography,
14
13
  } from 'antd';
15
14
  import {
@@ -23,6 +22,8 @@ import {
23
22
  } from '@ant-design/icons';
24
23
  import { useNavigate, useParams } from 'react-router-dom';
25
24
  import { customRequest } from 'helpers/http';
25
+ import { EnhancedFlowRenderer } from './components/EnhancedFlowRenderer';
26
+ import type { WorkflowDSL } from './types';
26
27
 
27
28
  const { Title, Text } = Typography;
28
29
 
@@ -67,6 +68,7 @@ interface InstanceDetail {
67
68
  createdAt: string;
68
69
  workflow: { name: string };
69
70
  nodeExecutions: NodeExecution[];
71
+ dsl?: WorkflowDSL;
70
72
  }
71
73
 
72
74
  function formatDuration(start: string | null, end: string | null): string {
@@ -102,6 +104,8 @@ export default function WorkflowInstanceDetailPage() {
102
104
  const [data, setData] = useState<InstanceDetail | null>(null);
103
105
  const [loading, setLoading] = useState(true);
104
106
  const [actionLoading, setActionLoading] = useState(false);
107
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
108
+ const [viewMode, setViewMode] = useState<'timeline' | 'canvas'>('canvas');
105
109
 
106
110
  const fetchData = useCallback(async () => {
107
111
  if (!instanceId) return;
@@ -130,6 +134,13 @@ export default function WorkflowInstanceDetailPage() {
130
134
  }
131
135
  }, [data?.status, fetchData]);
132
136
 
137
+ // Fallback to timeline if no DSL available
138
+ useEffect(() => {
139
+ if (data && !data.dsl && viewMode === 'canvas') {
140
+ setViewMode('timeline');
141
+ }
142
+ }, [data, viewMode]);
143
+
133
144
  const handleCancel = async () => {
134
145
  setActionLoading(true);
135
146
  try {
@@ -193,6 +204,13 @@ export default function WorkflowInstanceDetailPage() {
193
204
  if (!data) return null;
194
205
 
195
206
  const statusCfg = STATUS_TAG[data.status] || STATUS_TAG.PENDING;
207
+ const selectedExecution = selectedNodeId
208
+ ? data.nodeExecutions.find((ne) => ne.nodeId === selectedNodeId)
209
+ : null;
210
+ const selectedDslNode =
211
+ selectedNodeId && data.dsl
212
+ ? data.dsl.nodes.find((n) => n.id === selectedNodeId)
213
+ : null;
196
214
 
197
215
  return (
198
216
  <div
@@ -277,65 +295,456 @@ export default function WorkflowInstanceDetailPage() {
277
295
  </Descriptions>
278
296
  </Card>
279
297
 
280
- {/* Node Execution Timeline */}
281
- <Card title="Node Execution Timeline">
282
- {data.nodeExecutions.length === 0 ? (
283
- <Text type="secondary">No node executions recorded yet.</Text>
284
- ) : (
285
- <Steps
286
- direction="vertical"
287
- size="small"
288
- current={-1}
289
- items={data.nodeExecutions.map((ne) => {
290
- const isApprovalPending =
291
- ne.nodeType === 'approval' && ne.status === 'RUNNING';
292
- return {
293
- title: (
294
- <Space>
295
- <span>{ne.nodeLabel || ne.nodeId}</span>
296
- <Tag style={{ fontSize: 11 }}>{ne.nodeType}</Tag>
297
- {isApprovalPending && (
298
- <Space size={4}>
299
- <Button
300
- size="small"
301
- type="primary"
302
- onClick={() => handleApprove(ne.nodeId, true)}
303
- loading={actionLoading}
298
+ {/* Node Execution Timeline / Canvas View */}
299
+ <Card
300
+ title={
301
+ viewMode === 'timeline'
302
+ ? 'Node Execution Timeline'
303
+ : 'Flow Execution Visualization'
304
+ }
305
+ extra={
306
+ data.dsl ? (
307
+ <Space>
308
+ <Button.Group>
309
+ <Button
310
+ type={viewMode === 'canvas' ? 'primary' : 'default'}
311
+ onClick={() => {
312
+ setViewMode('canvas');
313
+ setSelectedNodeId(null);
314
+ }}
315
+ >
316
+ Canvas
317
+ </Button>
318
+ <Button
319
+ type={viewMode === 'timeline' ? 'primary' : 'default'}
320
+ onClick={() => {
321
+ setViewMode('timeline');
322
+ setSelectedNodeId(null);
323
+ }}
324
+ >
325
+ Timeline
326
+ </Button>
327
+ </Button.Group>
328
+ </Space>
329
+ ) : null
330
+ }
331
+ >
332
+ {viewMode === 'timeline' ? (
333
+ // Timeline View
334
+ <div style={{ display: 'flex', gap: 16 }}>
335
+ <div style={{ flex: 1 }}>
336
+ {data.nodeExecutions.length === 0 ? (
337
+ <Text type="secondary">No node executions recorded yet.</Text>
338
+ ) : (
339
+ <Steps
340
+ direction="vertical"
341
+ size="small"
342
+ current={-1}
343
+ items={data.nodeExecutions.map((ne) => {
344
+ const isApprovalPending =
345
+ ne.nodeType === 'approval' && ne.status === 'RUNNING';
346
+ const isSelected = selectedNodeId === ne.nodeId;
347
+ return {
348
+ title: (
349
+ <Space
350
+ style={{ cursor: 'pointer' }}
351
+ onClick={() =>
352
+ setSelectedNodeId(isSelected ? null : ne.nodeId)
353
+ }
304
354
  >
305
- Approve
306
- </Button>
307
- <Button
308
- size="small"
309
- danger
310
- onClick={() => handleApprove(ne.nodeId, false)}
311
- loading={actionLoading}
355
+ <span style={{ fontWeight: isSelected ? 600 : 400 }}>
356
+ {ne.nodeLabel || ne.nodeId}
357
+ </span>
358
+ <Tag style={{ fontSize: 11 }}>{ne.nodeType}</Tag>
359
+ {isApprovalPending && (
360
+ <Space size={4}>
361
+ <Button
362
+ size="small"
363
+ type="primary"
364
+ onClick={(e) => {
365
+ e.stopPropagation();
366
+ handleApprove(ne.nodeId, true);
367
+ }}
368
+ loading={actionLoading}
369
+ >
370
+ Approve
371
+ </Button>
372
+ <Button
373
+ size="small"
374
+ danger
375
+ onClick={(e) => {
376
+ e.stopPropagation();
377
+ handleApprove(ne.nodeId, false);
378
+ }}
379
+ loading={actionLoading}
380
+ >
381
+ Reject
382
+ </Button>
383
+ </Space>
384
+ )}
385
+ </Space>
386
+ ),
387
+ description: (
388
+ <div
389
+ style={{
390
+ fontSize: 12,
391
+ color: '#666',
392
+ cursor: 'pointer',
393
+ }}
394
+ onClick={() =>
395
+ setSelectedNodeId(isSelected ? null : ne.nodeId)
396
+ }
312
397
  >
313
- Reject
314
- </Button>
315
- </Space>
398
+ <span>
399
+ Duration:{' '}
400
+ {formatDuration(ne.startedAt, ne.finishedAt)}
401
+ </span>
402
+ {ne.error && (
403
+ <div style={{ color: '#ff4d4f', marginTop: 4 }}>
404
+ Error:{' '}
405
+ {typeof ne.error === 'object'
406
+ ? JSON.stringify(ne.error)
407
+ : String(ne.error)}
408
+ </div>
409
+ )}
410
+ </div>
411
+ ),
412
+ status: getStepStatus(ne.status),
413
+ icon:
414
+ ne.status === 'RUNNING' ? (
415
+ <LoadingOutlined />
416
+ ) : undefined,
417
+ };
418
+ })}
419
+ />
420
+ )}
421
+ </div>
422
+
423
+ {/* Node Detail Panel (shared with canvas view) */}
424
+ {selectedExecution && (
425
+ <div
426
+ style={{
427
+ width: 320,
428
+ borderLeft: '1px solid #f0f0f0',
429
+ paddingLeft: 16,
430
+ overflowY: 'auto',
431
+ }}
432
+ >
433
+ <div style={{ marginBottom: 16 }}>
434
+ <Title level={5}>
435
+ {selectedExecution.nodeLabel || selectedExecution.nodeId}
436
+ </Title>
437
+ <Tag
438
+ color={
439
+ selectedExecution.status === 'COMPLETED'
440
+ ? 'green'
441
+ : selectedExecution.status === 'RUNNING'
442
+ ? 'blue'
443
+ : selectedExecution.status === 'FAILED'
444
+ ? 'red'
445
+ : 'gold'
446
+ }
447
+ >
448
+ {selectedExecution.status}
449
+ </Tag>
450
+ </div>
451
+
452
+ <Descriptions
453
+ column={1}
454
+ size="small"
455
+ style={{ marginBottom: 16 }}
456
+ >
457
+ <Descriptions.Item label="Node Type">
458
+ {selectedExecution.nodeType}
459
+ </Descriptions.Item>
460
+ <Descriptions.Item label="Started">
461
+ {selectedExecution.startedAt
462
+ ? new Date(selectedExecution.startedAt).toLocaleString()
463
+ : '—'}
464
+ </Descriptions.Item>
465
+ <Descriptions.Item label="Finished">
466
+ {selectedExecution.finishedAt
467
+ ? new Date(selectedExecution.finishedAt).toLocaleString()
468
+ : '—'}
469
+ </Descriptions.Item>
470
+ <Descriptions.Item label="Duration">
471
+ {formatDuration(
472
+ selectedExecution.startedAt,
473
+ selectedExecution.finishedAt,
316
474
  )}
317
- </Space>
318
- ),
319
- description: (
320
- <div style={{ fontSize: 12, color: '#666' }}>
321
- <span>
322
- Duration: {formatDuration(ne.startedAt, ne.finishedAt)}
323
- </span>
324
- {ne.error && (
325
- <div style={{ color: '#ff4d4f', marginTop: 4 }}>
326
- Error:{' '}
327
- {typeof ne.error === 'object'
328
- ? JSON.stringify(ne.error)
329
- : String(ne.error)}
475
+ </Descriptions.Item>
476
+ </Descriptions>
477
+
478
+ {selectedExecution.input && (
479
+ <div style={{ marginBottom: 12 }}>
480
+ <Text strong style={{ fontSize: 12 }}>
481
+ Input:
482
+ </Text>
483
+ <pre
484
+ style={{
485
+ background: '#fafafa',
486
+ padding: 8,
487
+ borderRadius: 4,
488
+ fontSize: 11,
489
+ maxHeight: 150,
490
+ overflow: 'auto',
491
+ }}
492
+ >
493
+ {JSON.stringify(selectedExecution.input, null, 2)}
494
+ </pre>
495
+ </div>
496
+ )}
497
+
498
+ {selectedExecution.output && (
499
+ <div style={{ marginBottom: 12 }}>
500
+ <Text strong style={{ fontSize: 12 }}>
501
+ Output:
502
+ </Text>
503
+ <pre
504
+ style={{
505
+ background: '#fafafa',
506
+ padding: 8,
507
+ borderRadius: 4,
508
+ fontSize: 11,
509
+ maxHeight: 150,
510
+ overflow: 'auto',
511
+ }}
512
+ >
513
+ {JSON.stringify(selectedExecution.output, null, 2)}
514
+ </pre>
515
+ </div>
516
+ )}
517
+
518
+ {selectedExecution.error && (
519
+ <div style={{ marginBottom: 12 }}>
520
+ <Text strong style={{ fontSize: 12, color: '#ff4d4f' }}>
521
+ Error:
522
+ </Text>
523
+ <pre
524
+ style={{
525
+ background: '#fff1f0',
526
+ padding: 8,
527
+ borderRadius: 4,
528
+ fontSize: 11,
529
+ color: '#ff4d4f',
530
+ maxHeight: 150,
531
+ overflow: 'auto',
532
+ }}
533
+ >
534
+ {typeof selectedExecution.error === 'object'
535
+ ? JSON.stringify(selectedExecution.error, null, 2)
536
+ : String(selectedExecution.error)}
537
+ </pre>
538
+ </div>
539
+ )}
540
+ </div>
541
+ )}
542
+ </div>
543
+ ) : (
544
+ // Canvas View
545
+ <>
546
+ {data.dsl ? (
547
+ <div style={{ display: 'flex', gap: 16 }}>
548
+ {/* Canvas */}
549
+ <div style={{ flex: 1, height: 500, minHeight: 500 }}>
550
+ <EnhancedFlowRenderer
551
+ dsl={data.dsl}
552
+ nodeExecutions={data.nodeExecutions}
553
+ showExecutionStatus={true}
554
+ selectedNodeId={selectedNodeId}
555
+ readonly={true}
556
+ onNodeClick={(nodeId) => setSelectedNodeId(nodeId)}
557
+ />
558
+ </div>
559
+
560
+ {/* Node Detail Panel */}
561
+ {selectedNodeId && (
562
+ <div
563
+ style={{
564
+ width: 320,
565
+ borderLeft: '1px solid #f0f0f0',
566
+ paddingLeft: 16,
567
+ overflowY: 'auto',
568
+ }}
569
+ >
570
+ {selectedExecution ? (
571
+ <>
572
+ <div style={{ marginBottom: 16 }}>
573
+ <Title level={5}>
574
+ {selectedExecution.nodeLabel ||
575
+ selectedExecution.nodeId}
576
+ </Title>
577
+ <Tag
578
+ color={
579
+ selectedExecution.status === 'COMPLETED'
580
+ ? 'green'
581
+ : selectedExecution.status === 'RUNNING'
582
+ ? 'blue'
583
+ : selectedExecution.status === 'FAILED'
584
+ ? 'red'
585
+ : 'gold'
586
+ }
587
+ >
588
+ {selectedExecution.status}
589
+ </Tag>
590
+ </div>
591
+
592
+ <Descriptions
593
+ column={1}
594
+ size="small"
595
+ style={{ marginBottom: 16 }}
596
+ >
597
+ <Descriptions.Item label="Node Type">
598
+ {selectedExecution.nodeType}
599
+ </Descriptions.Item>
600
+ <Descriptions.Item label="Started">
601
+ {selectedExecution.startedAt
602
+ ? new Date(
603
+ selectedExecution.startedAt,
604
+ ).toLocaleString()
605
+ : '—'}
606
+ </Descriptions.Item>
607
+ <Descriptions.Item label="Finished">
608
+ {selectedExecution.finishedAt
609
+ ? new Date(
610
+ selectedExecution.finishedAt,
611
+ ).toLocaleString()
612
+ : '—'}
613
+ </Descriptions.Item>
614
+ <Descriptions.Item label="Duration">
615
+ {formatDuration(
616
+ selectedExecution.startedAt,
617
+ selectedExecution.finishedAt,
618
+ )}
619
+ </Descriptions.Item>
620
+ </Descriptions>
621
+
622
+ {selectedExecution.input && (
623
+ <div style={{ marginBottom: 12 }}>
624
+ <Text strong style={{ fontSize: 12 }}>
625
+ Input:
626
+ </Text>
627
+ <pre
628
+ style={{
629
+ background: '#fafafa',
630
+ padding: 8,
631
+ borderRadius: 4,
632
+ fontSize: 11,
633
+ maxHeight: 150,
634
+ overflow: 'auto',
635
+ }}
636
+ >
637
+ {JSON.stringify(selectedExecution.input, null, 2)}
638
+ </pre>
639
+ </div>
640
+ )}
641
+
642
+ {selectedExecution.output && (
643
+ <div style={{ marginBottom: 12 }}>
644
+ <Text strong style={{ fontSize: 12 }}>
645
+ Output:
646
+ </Text>
647
+ <pre
648
+ style={{
649
+ background: '#fafafa',
650
+ padding: 8,
651
+ borderRadius: 4,
652
+ fontSize: 11,
653
+ maxHeight: 150,
654
+ overflow: 'auto',
655
+ }}
656
+ >
657
+ {JSON.stringify(
658
+ selectedExecution.output,
659
+ null,
660
+ 2,
661
+ )}
662
+ </pre>
663
+ </div>
664
+ )}
665
+
666
+ {selectedExecution.error && (
667
+ <div style={{ marginBottom: 12 }}>
668
+ <Text
669
+ strong
670
+ style={{ fontSize: 12, color: '#ff4d4f' }}
671
+ >
672
+ Error:
673
+ </Text>
674
+ <pre
675
+ style={{
676
+ background: '#fff1f0',
677
+ padding: 8,
678
+ borderRadius: 4,
679
+ fontSize: 11,
680
+ color: '#ff4d4f',
681
+ maxHeight: 150,
682
+ overflow: 'auto',
683
+ }}
684
+ >
685
+ {typeof selectedExecution.error === 'object'
686
+ ? JSON.stringify(
687
+ selectedExecution.error,
688
+ null,
689
+ 2,
690
+ )
691
+ : String(selectedExecution.error)}
692
+ </pre>
693
+ </div>
694
+ )}
695
+
696
+ {/* Approval actions */}
697
+ {selectedExecution.nodeType === 'approval' &&
698
+ selectedExecution.status === 'RUNNING' && (
699
+ <Space style={{ width: '100%' }}>
700
+ <Button
701
+ type="primary"
702
+ block
703
+ onClick={() =>
704
+ handleApprove(selectedExecution.nodeId, true)
705
+ }
706
+ loading={actionLoading}
707
+ >
708
+ Approve
709
+ </Button>
710
+ <Button
711
+ danger
712
+ block
713
+ onClick={() =>
714
+ handleApprove(selectedExecution.nodeId, false)
715
+ }
716
+ loading={actionLoading}
717
+ >
718
+ Reject
719
+ </Button>
720
+ </Space>
721
+ )}
722
+ </>
723
+ ) : selectedDslNode ? (
724
+ <div>
725
+ <div style={{ marginBottom: 16 }}>
726
+ <Title level={5}>{selectedDslNode.label}</Title>
727
+ <Tag color="default">Not Executed</Tag>
728
+ </div>
729
+ <Descriptions column={1} size="small">
730
+ <Descriptions.Item label="Node Type">
731
+ {selectedDslNode.type}
732
+ </Descriptions.Item>
733
+ <Descriptions.Item label="Status">
734
+ <Text type="secondary">Waiting for execution</Text>
735
+ </Descriptions.Item>
736
+ </Descriptions>
330
737
  </div>
331
- )}
738
+ ) : null}
332
739
  </div>
333
- ),
334
- status: getStepStatus(ne.status),
335
- icon: ne.status === 'RUNNING' ? <LoadingOutlined /> : undefined,
336
- };
337
- })}
338
- />
740
+ )}
741
+ </div>
742
+ ) : (
743
+ <Text type="secondary">
744
+ Workflow DSL not available for visualization.
745
+ </Text>
746
+ )}
747
+ </>
339
748
  )}
340
749
  </Card>
341
750
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [