@principal-ai/principal-view-react 0.14.25 → 0.14.27
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/SequenceDiagramRenderer.d.ts.map +1 -1
- package/dist/components/SequenceDiagramRenderer.js +83 -7
- package/dist/components/SequenceDiagramRenderer.js.map +1 -1
- package/dist/components/WorkflowSequenceDiagram.d.ts +52 -0
- package/dist/components/WorkflowSequenceDiagram.d.ts.map +1 -0
- package/dist/components/WorkflowSequenceDiagram.js +127 -0
- package/dist/components/WorkflowSequenceDiagram.js.map +1 -0
- package/dist/hooks/useSequenceLayout.d.ts +4 -0
- package/dist/hooks/useSequenceLayout.d.ts.map +1 -1
- package/dist/hooks/useSequenceLayout.js +66 -25
- package/dist/hooks/useSequenceLayout.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/SequenceDiagramRenderer.tsx +162 -7
- package/src/components/WorkflowSequenceDiagram.tsx +222 -0
- package/src/hooks/useSequenceLayout.ts +73 -28
- package/src/index.ts +3 -0
- package/src/stories/FileCitySequence.stories.tsx +544 -0
- package/src/stories/data/file-city-images-transformed.otel.canvas.json +295 -0
- package/src/stories/data/file-city-workflows.json +269 -0
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Minimal marker node for arrow-centric sequence diagrams
|
|
39
|
+
* Invisible - just used for positioning, all rendering done by edges
|
|
39
40
|
*/
|
|
40
41
|
function SequenceMarkerNode({ data }: NodeProps) {
|
|
41
42
|
return (
|
|
@@ -43,10 +44,7 @@ function SequenceMarkerNode({ data }: NodeProps) {
|
|
|
43
44
|
style={{
|
|
44
45
|
width: '100%',
|
|
45
46
|
height: '100%',
|
|
46
|
-
|
|
47
|
-
borderRadius: '50%',
|
|
48
|
-
border: '2px solid var(--sequence-marker-border, #4169E1)',
|
|
49
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
47
|
+
opacity: 0,
|
|
50
48
|
}}
|
|
51
49
|
title={data.fullName as string}
|
|
52
50
|
>
|
|
@@ -65,7 +63,7 @@ function SequenceMarkerNode({ data }: NodeProps) {
|
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
/**
|
|
68
|
-
* Sequence arrow edge with label
|
|
66
|
+
* Sequence arrow edge with label (dot to dot)
|
|
69
67
|
*/
|
|
70
68
|
function SequenceArrowEdge({
|
|
71
69
|
id,
|
|
@@ -131,6 +129,146 @@ function SequenceArrowEdge({
|
|
|
131
129
|
);
|
|
132
130
|
}
|
|
133
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Participant-to-participant arrow edge (or activation bar for same-lane)
|
|
134
|
+
* Draws from source participant lifeline to target participant lifeline
|
|
135
|
+
*/
|
|
136
|
+
function SequenceArrowParticipantEdge({
|
|
137
|
+
id,
|
|
138
|
+
sourceY,
|
|
139
|
+
targetY,
|
|
140
|
+
label,
|
|
141
|
+
data,
|
|
142
|
+
}: EdgeProps) {
|
|
143
|
+
const { theme } = useTheme();
|
|
144
|
+
|
|
145
|
+
// Use participant X positions from data (swimlane centers)
|
|
146
|
+
const sourceX = (data?.sourceParticipantX ?? 0) as number;
|
|
147
|
+
const targetX = (data?.targetParticipantX ?? 0) as number;
|
|
148
|
+
const safeSourceY = (sourceY ?? 0) as number;
|
|
149
|
+
const safeTargetY = (targetY ?? 0) as number;
|
|
150
|
+
|
|
151
|
+
// Check if this is same-lane (activation bar) or cross-lane (arrow)
|
|
152
|
+
const isSameLane = sourceX === targetX;
|
|
153
|
+
const isLastEvent = data?.isLastEvent === true;
|
|
154
|
+
|
|
155
|
+
// Style based on whether it's a move event (IPC) or transform event (internal)
|
|
156
|
+
const isMoveEvent = data?.isMoveEvent === true;
|
|
157
|
+
const strokeColor = isMoveEvent ? (theme.colors.accent || '#f48771') : theme.colors.primary;
|
|
158
|
+
|
|
159
|
+
// Same lane: render as activation bar
|
|
160
|
+
if (isSameLane) {
|
|
161
|
+
const barWidth = 12;
|
|
162
|
+
const eventSpacing = (data?.eventSpacing ?? 80) as number;
|
|
163
|
+
|
|
164
|
+
// For last event, use half the event spacing for bar height
|
|
165
|
+
let barHeight: number;
|
|
166
|
+
let barY: number;
|
|
167
|
+
|
|
168
|
+
if (isLastEvent) {
|
|
169
|
+
barHeight = eventSpacing / 2;
|
|
170
|
+
barY = safeSourceY; // Start at the event position
|
|
171
|
+
} else {
|
|
172
|
+
// Normal case: bar from source to target
|
|
173
|
+
const calculatedHeight = Math.abs(safeTargetY - safeSourceY);
|
|
174
|
+
// Ensure minimum height if events are at same position
|
|
175
|
+
barHeight = calculatedHeight > 0 ? calculatedHeight : eventSpacing / 2;
|
|
176
|
+
barY = Math.min(safeSourceY, safeTargetY);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const barX = sourceX - barWidth / 2;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
{/* Activation bar */}
|
|
184
|
+
<svg>
|
|
185
|
+
<rect
|
|
186
|
+
x={barX}
|
|
187
|
+
y={barY}
|
|
188
|
+
width={barWidth}
|
|
189
|
+
height={barHeight}
|
|
190
|
+
fill={strokeColor}
|
|
191
|
+
fillOpacity={0.15}
|
|
192
|
+
stroke={strokeColor}
|
|
193
|
+
strokeWidth={2}
|
|
194
|
+
rx={2}
|
|
195
|
+
/>
|
|
196
|
+
</svg>
|
|
197
|
+
{label && (
|
|
198
|
+
<EdgeLabelRenderer>
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
position: 'absolute',
|
|
202
|
+
transform: `translate(0, -50%) translate(${sourceX + 15}px,${barY + barHeight / 2}px)`,
|
|
203
|
+
background: theme.colors.background,
|
|
204
|
+
padding: '2px 8px',
|
|
205
|
+
borderRadius: 4,
|
|
206
|
+
fontSize: theme.fontSizes[0],
|
|
207
|
+
fontWeight: theme.fontWeights.medium,
|
|
208
|
+
fontFamily: theme.fonts.body,
|
|
209
|
+
color: strokeColor,
|
|
210
|
+
border: `1px solid ${strokeColor}`,
|
|
211
|
+
pointerEvents: 'all',
|
|
212
|
+
whiteSpace: 'nowrap',
|
|
213
|
+
}}
|
|
214
|
+
className="nodrag nopan"
|
|
215
|
+
>
|
|
216
|
+
{label}
|
|
217
|
+
</div>
|
|
218
|
+
</EdgeLabelRenderer>
|
|
219
|
+
)}
|
|
220
|
+
</>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Cross-lane: render as horizontal arrow at midpoint
|
|
225
|
+
const strokeWidth = isMoveEvent ? 2.5 : 2;
|
|
226
|
+
const markerEnd = isMoveEvent ? 'url(#sequence-arrow-move)' : 'url(#sequence-arrow)';
|
|
227
|
+
|
|
228
|
+
// Draw horizontal arrow at the midpoint between source and target Y positions
|
|
229
|
+
const arrowY = (safeSourceY + safeTargetY) / 2;
|
|
230
|
+
const path = `M ${sourceX} ${arrowY} L ${targetX} ${arrowY}`;
|
|
231
|
+
const labelX = (sourceX + targetX) / 2;
|
|
232
|
+
const labelY = arrowY;
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<BaseEdge
|
|
237
|
+
id={id}
|
|
238
|
+
path={path}
|
|
239
|
+
style={{
|
|
240
|
+
stroke: strokeColor,
|
|
241
|
+
strokeWidth: strokeWidth,
|
|
242
|
+
}}
|
|
243
|
+
markerEnd={markerEnd}
|
|
244
|
+
/>
|
|
245
|
+
{label && (
|
|
246
|
+
<EdgeLabelRenderer>
|
|
247
|
+
<div
|
|
248
|
+
style={{
|
|
249
|
+
position: 'absolute',
|
|
250
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY - 12}px)`,
|
|
251
|
+
background: theme.colors.background,
|
|
252
|
+
padding: isMoveEvent ? '3px 10px' : '2px 8px',
|
|
253
|
+
borderRadius: 4,
|
|
254
|
+
fontSize: theme.fontSizes[0],
|
|
255
|
+
fontWeight: isMoveEvent ? theme.fontWeights.bold : theme.fontWeights.medium,
|
|
256
|
+
fontFamily: theme.fonts.body,
|
|
257
|
+
color: strokeColor,
|
|
258
|
+
border: isMoveEvent ? `2px solid ${strokeColor}` : `1px solid ${strokeColor}`,
|
|
259
|
+
pointerEvents: 'all',
|
|
260
|
+
whiteSpace: 'nowrap',
|
|
261
|
+
}}
|
|
262
|
+
className="nodrag nopan"
|
|
263
|
+
>
|
|
264
|
+
{label}
|
|
265
|
+
</div>
|
|
266
|
+
</EdgeLabelRenderer>
|
|
267
|
+
)}
|
|
268
|
+
</>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
134
272
|
/**
|
|
135
273
|
* Default node types including sequence marker
|
|
136
274
|
*/
|
|
@@ -139,10 +277,11 @@ const defaultSequenceNodeTypes: NodeTypes = {
|
|
|
139
277
|
};
|
|
140
278
|
|
|
141
279
|
/**
|
|
142
|
-
* Default edge types including sequence arrow
|
|
280
|
+
* Default edge types including sequence arrow and participant arrow
|
|
143
281
|
*/
|
|
144
282
|
const defaultSequenceEdgeTypes: EdgeTypes = {
|
|
145
283
|
sequenceArrow: SequenceArrowEdge,
|
|
284
|
+
sequenceArrowParticipant: SequenceArrowParticipantEdge,
|
|
146
285
|
};
|
|
147
286
|
|
|
148
287
|
/**
|
|
@@ -394,9 +533,10 @@ function SequenceDiagramInner({
|
|
|
394
533
|
zoomOnScroll
|
|
395
534
|
style={{ background: theme.colors.background }}
|
|
396
535
|
>
|
|
397
|
-
{/* SVG defs for arrow
|
|
536
|
+
{/* SVG defs for arrow markers */}
|
|
398
537
|
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
|
399
538
|
<defs>
|
|
539
|
+
{/* Standard arrow for transform events */}
|
|
400
540
|
<marker
|
|
401
541
|
id="sequence-arrow"
|
|
402
542
|
viewBox="0 0 10 10"
|
|
@@ -411,6 +551,21 @@ function SequenceDiagramInner({
|
|
|
411
551
|
fill={theme.colors.primary}
|
|
412
552
|
/>
|
|
413
553
|
</marker>
|
|
554
|
+
{/* Accent arrow for move events (IPC calls) */}
|
|
555
|
+
<marker
|
|
556
|
+
id="sequence-arrow-move"
|
|
557
|
+
viewBox="0 0 10 10"
|
|
558
|
+
refX="8"
|
|
559
|
+
refY="5"
|
|
560
|
+
markerWidth="7"
|
|
561
|
+
markerHeight="7"
|
|
562
|
+
orient="auto-start-reverse"
|
|
563
|
+
>
|
|
564
|
+
<path
|
|
565
|
+
d="M 0 0 L 10 5 L 0 10 z"
|
|
566
|
+
fill={theme.colors.accent || '#f48771'}
|
|
567
|
+
/>
|
|
568
|
+
</marker>
|
|
414
569
|
</defs>
|
|
415
570
|
</svg>
|
|
416
571
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Sequence Diagram
|
|
3
|
+
*
|
|
4
|
+
* A wrapper around SequenceDiagramRenderer that handles conversion from
|
|
5
|
+
* workflow scenarios to sequence diagram format, following the graph-to-sequence
|
|
6
|
+
* model where participants are scopes and events are move/transform operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
import type { WorkflowScenario, Canvas, ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
11
|
+
import { SequenceDiagramRenderer } from './SequenceDiagramRenderer';
|
|
12
|
+
import type { SequenceEvent, SequenceEdge } from '../hooks/useSequenceLayout';
|
|
13
|
+
import type { UseSequenceLayoutOptions } from '../hooks/useSequenceLayout';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for WorkflowSequenceDiagram
|
|
17
|
+
*/
|
|
18
|
+
export interface WorkflowSequenceDiagramProps {
|
|
19
|
+
/** Workflow scenario to visualize */
|
|
20
|
+
scenario: WorkflowScenario;
|
|
21
|
+
|
|
22
|
+
/** Optional canvas for extracting scope metadata from OTEL nodes */
|
|
23
|
+
canvas?: Canvas | ExtendedCanvas | null;
|
|
24
|
+
|
|
25
|
+
/** Optional height for the diagram */
|
|
26
|
+
height?: number | string;
|
|
27
|
+
|
|
28
|
+
/** Optional width for the diagram */
|
|
29
|
+
width?: number | string;
|
|
30
|
+
|
|
31
|
+
/** Layout options for the sequence diagram */
|
|
32
|
+
layoutOptions?: UseSequenceLayoutOptions;
|
|
33
|
+
|
|
34
|
+
/** Whether to show controls */
|
|
35
|
+
showControls?: boolean;
|
|
36
|
+
|
|
37
|
+
/** Whether to show background grid */
|
|
38
|
+
showBackground?: boolean;
|
|
39
|
+
|
|
40
|
+
/** Optional class name */
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type guard to check if a node is an OTEL event node
|
|
46
|
+
*/
|
|
47
|
+
function isOtelEventNode(node: any): node is {
|
|
48
|
+
type: 'otel-event';
|
|
49
|
+
event: { name: string };
|
|
50
|
+
otel?: { scope?: string };
|
|
51
|
+
label?: string;
|
|
52
|
+
} {
|
|
53
|
+
return node?.type === 'otel-event' && node?.event?.name;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Type guard to check if a node is an OTEL participant node
|
|
58
|
+
*/
|
|
59
|
+
function isOtelParticipantNode(node: any): node is {
|
|
60
|
+
type: 'otel-participant';
|
|
61
|
+
participant: { name: string; scope?: string };
|
|
62
|
+
transformEvents?: Array<{ name: string }>;
|
|
63
|
+
} {
|
|
64
|
+
return node?.type === 'otel-participant' && node?.participant?.name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert workflow scenario to sequence diagram format
|
|
69
|
+
*/
|
|
70
|
+
function convertWorkflowToSequence(
|
|
71
|
+
scenario: WorkflowScenario,
|
|
72
|
+
canvas?: Canvas | ExtendedCanvas | null
|
|
73
|
+
): {
|
|
74
|
+
events: SequenceEvent[];
|
|
75
|
+
edges: SequenceEdge[];
|
|
76
|
+
} {
|
|
77
|
+
// Extract event names from scenario template
|
|
78
|
+
const templateEvents = scenario.template.events || {};
|
|
79
|
+
const eventNames = Object.keys(templateEvents);
|
|
80
|
+
|
|
81
|
+
if (eventNames.length === 0) {
|
|
82
|
+
return { events: [], edges: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build maps from canvas metadata
|
|
86
|
+
const eventToScopeMap = new Map<string, string>();
|
|
87
|
+
const eventToLabelMap = new Map<string, string>();
|
|
88
|
+
const eventToMoveEventMap = new Map<string, boolean>();
|
|
89
|
+
|
|
90
|
+
if (canvas?.nodes) {
|
|
91
|
+
for (const node of canvas.nodes) {
|
|
92
|
+
// Type guard narrowing - we know the structure but Canvas type doesn't include OTEL specifics
|
|
93
|
+
const nodeData = node as Record<string, any>;
|
|
94
|
+
|
|
95
|
+
// Handle OTEL event nodes
|
|
96
|
+
if (isOtelEventNode(nodeData)) {
|
|
97
|
+
const eventName = nodeData.event.name;
|
|
98
|
+
|
|
99
|
+
if (nodeData.otel?.scope) {
|
|
100
|
+
eventToScopeMap.set(eventName, nodeData.otel.scope);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (nodeData.label) {
|
|
104
|
+
eventToLabelMap.set(eventName, nodeData.label);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle OTEL participant nodes (participant-based model)
|
|
109
|
+
if (isOtelParticipantNode(nodeData)) {
|
|
110
|
+
const scope = nodeData.participant.scope || nodeData.participant.name;
|
|
111
|
+
|
|
112
|
+
// Mark transform events (internal to participant)
|
|
113
|
+
if (nodeData.transformEvents) {
|
|
114
|
+
for (const transformEvent of nodeData.transformEvents) {
|
|
115
|
+
eventToScopeMap.set(transformEvent.name, scope);
|
|
116
|
+
eventToMoveEventMap.set(transformEvent.name, false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Convert to SequenceEvent format
|
|
124
|
+
const events: SequenceEvent[] = eventNames.map((eventName, index) => {
|
|
125
|
+
const scope = eventToScopeMap.get(eventName) || 'unknown';
|
|
126
|
+
const label = eventToLabelMap.get(eventName) || eventName.split('.').pop() || eventName;
|
|
127
|
+
|
|
128
|
+
// Determine if this is a move event
|
|
129
|
+
// Default to true unless explicitly marked as transform event in participant node
|
|
130
|
+
const isMoveEvent = eventToMoveEventMap.get(eventName) ?? true;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: `event-${index}`,
|
|
134
|
+
name: `${scope}.${eventName}`,
|
|
135
|
+
label,
|
|
136
|
+
moveEvent: isMoveEvent,
|
|
137
|
+
participant: scope,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create sequential edges
|
|
142
|
+
const edges: SequenceEdge[] = [];
|
|
143
|
+
for (let i = 0; i < events.length - 1; i++) {
|
|
144
|
+
edges.push({
|
|
145
|
+
id: `edge-${i}`,
|
|
146
|
+
fromEvent: events[i].id,
|
|
147
|
+
toEvent: events[i + 1].id,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { events, edges };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* WorkflowSequenceDiagram Component
|
|
156
|
+
*
|
|
157
|
+
* Renders a workflow scenario as a sequence diagram, automatically extracting
|
|
158
|
+
* participant scopes from the canvas and distinguishing between move events
|
|
159
|
+
* (cross-participant communication) and transform events (internal processing).
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <WorkflowSequenceDiagram
|
|
164
|
+
* scenario={workflowScenario}
|
|
165
|
+
* canvas={otelCanvas}
|
|
166
|
+
* height={600}
|
|
167
|
+
* layoutOptions={{
|
|
168
|
+
* namespaceStrategy: 'first',
|
|
169
|
+
* eventSpacing: 80,
|
|
170
|
+
* }}
|
|
171
|
+
* />
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function WorkflowSequenceDiagram({
|
|
175
|
+
scenario,
|
|
176
|
+
canvas,
|
|
177
|
+
height = 600,
|
|
178
|
+
width = '100%',
|
|
179
|
+
layoutOptions,
|
|
180
|
+
showControls = true,
|
|
181
|
+
showBackground = false,
|
|
182
|
+
className,
|
|
183
|
+
}: WorkflowSequenceDiagramProps) {
|
|
184
|
+
// Convert workflow to sequence format
|
|
185
|
+
const { events, edges } = useMemo(
|
|
186
|
+
() => convertWorkflowToSequence(scenario, canvas),
|
|
187
|
+
[scenario, canvas]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// If no events, show empty state
|
|
191
|
+
if (events.length === 0) {
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
className={className}
|
|
195
|
+
style={{
|
|
196
|
+
width,
|
|
197
|
+
height,
|
|
198
|
+
display: 'flex',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
color: '#888',
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
No events in workflow
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Render sequence diagram
|
|
210
|
+
return (
|
|
211
|
+
<SequenceDiagramRenderer
|
|
212
|
+
events={events}
|
|
213
|
+
edges={edges}
|
|
214
|
+
height={height}
|
|
215
|
+
width={width}
|
|
216
|
+
layoutOptions={layoutOptions}
|
|
217
|
+
showControls={showControls}
|
|
218
|
+
showBackground={showBackground}
|
|
219
|
+
className={className}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -19,6 +19,10 @@ export interface SequenceEvent {
|
|
|
19
19
|
label?: string;
|
|
20
20
|
/** Optional event type for styling */
|
|
21
21
|
type?: string;
|
|
22
|
+
/** Whether this is a move event (crosses participant boundaries) */
|
|
23
|
+
moveEvent?: boolean;
|
|
24
|
+
/** Participant this event belongs to (for move events, this is the target) */
|
|
25
|
+
participant?: string;
|
|
22
26
|
/** Additional data to pass through to the node */
|
|
23
27
|
data?: Record<string, unknown>;
|
|
24
28
|
}
|
|
@@ -306,9 +310,6 @@ export function useSequenceLayout(
|
|
|
306
310
|
// This creates horizontal "time layers" across all swimlanes
|
|
307
311
|
const nodes: Node[] = [];
|
|
308
312
|
|
|
309
|
-
// Build event lookup for edge label resolution
|
|
310
|
-
const eventById = new Map(events.map((e) => [e.id, e]));
|
|
311
|
-
|
|
312
313
|
for (let i = 0; i < events.length; i++) {
|
|
313
314
|
const event = events[i];
|
|
314
315
|
const originalNamespace = eventNamespaces.get(event.id)!;
|
|
@@ -332,6 +333,7 @@ export function useSequenceLayout(
|
|
|
332
333
|
namespace: originalNamespace,
|
|
333
334
|
visibleNamespace,
|
|
334
335
|
timeLayer: i,
|
|
336
|
+
isMoveEvent: event.moveEvent === true,
|
|
335
337
|
...event.data,
|
|
336
338
|
},
|
|
337
339
|
style: {
|
|
@@ -341,32 +343,75 @@ export function useSequenceLayout(
|
|
|
341
343
|
});
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
// Step 5: Create edges
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const targetNamespace = eventNamespaces.get(edge.toEvent);
|
|
348
|
-
const crossesLanes =
|
|
349
|
-
namespaceToVisible.get(sourceNamespace!) !==
|
|
350
|
-
namespaceToVisible.get(targetNamespace!);
|
|
351
|
-
|
|
352
|
-
const targetEvent = eventById.get(edge.toEvent);
|
|
353
|
-
const edgeLabel = edge.label || targetEvent?.label || targetEvent?.name.split('.').pop() || '';
|
|
346
|
+
// Step 5: Create edges - one per event, showing how to get to the NEXT event
|
|
347
|
+
// Each edge looks forward to determine what to render
|
|
348
|
+
const edges: Edge[] = [];
|
|
354
349
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
350
|
+
for (let i = 0; i < events.length; i++) {
|
|
351
|
+
const currentEvent = events[i];
|
|
352
|
+
const currentNamespace = eventNamespaces.get(currentEvent.id)!;
|
|
353
|
+
const currentVisibleNs = namespaceToVisible.get(currentNamespace)!;
|
|
354
|
+
const currentLane = swimlaneByNamespace.get(currentVisibleNs)!;
|
|
355
|
+
|
|
356
|
+
// Look at the next event (if any)
|
|
357
|
+
if (i < events.length - 1) {
|
|
358
|
+
const nextEvent = events[i + 1];
|
|
359
|
+
const nextNamespace = eventNamespaces.get(nextEvent.id)!;
|
|
360
|
+
const nextVisibleNs = namespaceToVisible.get(nextNamespace)!;
|
|
361
|
+
const nextLane = swimlaneByNamespace.get(nextVisibleNs)!;
|
|
362
|
+
const nextIsMoveEvent = nextEvent.moveEvent === true;
|
|
363
|
+
const crossesLanes = currentVisibleNs !== nextVisibleNs;
|
|
364
|
+
|
|
365
|
+
// Label is from the CURRENT event (the one creating this edge)
|
|
366
|
+
const edgeLabel = currentEvent.label || currentEvent.name.split('.').pop() || currentEvent.name;
|
|
367
|
+
|
|
368
|
+
edges.push({
|
|
369
|
+
id: `edge-${currentEvent.id}-to-${nextEvent.id}`,
|
|
370
|
+
source: currentEvent.id,
|
|
371
|
+
target: nextEvent.id,
|
|
372
|
+
type: 'sequenceArrowParticipant',
|
|
373
|
+
label: edgeLabel,
|
|
374
|
+
labelStyle: { fontSize: 12, fontWeight: 500 },
|
|
375
|
+
labelBgStyle: { fill: 'white', fillOpacity: 0.8 },
|
|
376
|
+
data: {
|
|
377
|
+
crossesLanes,
|
|
378
|
+
sourceNamespace: currentNamespace,
|
|
379
|
+
targetNamespace: nextNamespace,
|
|
380
|
+
isMoveEvent: nextIsMoveEvent,
|
|
381
|
+
sourceEvent: currentEvent,
|
|
382
|
+
targetEvent: nextEvent,
|
|
383
|
+
sourceParticipantX: currentLane.x,
|
|
384
|
+
targetParticipantX: nextLane.x,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
// Last event - render small activation bar to show it exists
|
|
389
|
+
const currentIsMoveEvent = currentEvent.moveEvent === true;
|
|
390
|
+
const edgeLabel = currentEvent.label || currentEvent.name.split('.').pop() || currentEvent.name;
|
|
391
|
+
|
|
392
|
+
edges.push({
|
|
393
|
+
id: `edge-${currentEvent.id}-end`,
|
|
394
|
+
source: currentEvent.id,
|
|
395
|
+
target: currentEvent.id,
|
|
396
|
+
type: 'sequenceArrowParticipant',
|
|
397
|
+
label: edgeLabel,
|
|
398
|
+
labelStyle: { fontSize: 12, fontWeight: 500 },
|
|
399
|
+
labelBgStyle: { fill: 'white', fillOpacity: 0.8 },
|
|
400
|
+
data: {
|
|
401
|
+
crossesLanes: false,
|
|
402
|
+
sourceNamespace: currentNamespace,
|
|
403
|
+
targetNamespace: currentNamespace,
|
|
404
|
+
isMoveEvent: currentIsMoveEvent,
|
|
405
|
+
sourceEvent: currentEvent,
|
|
406
|
+
targetEvent: currentEvent,
|
|
407
|
+
sourceParticipantX: currentLane.x,
|
|
408
|
+
targetParticipantX: currentLane.x,
|
|
409
|
+
isLastEvent: true,
|
|
410
|
+
eventSpacing,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
370
415
|
|
|
371
416
|
// Step 6: Compute total dimensions
|
|
372
417
|
const totalWidth =
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,9 @@ export type {
|
|
|
40
40
|
export { SequenceDiagramRenderer } from './components/SequenceDiagramRenderer';
|
|
41
41
|
export type { SequenceDiagramRendererProps } from './components/SequenceDiagramRenderer';
|
|
42
42
|
|
|
43
|
+
export { WorkflowSequenceDiagram } from './components/WorkflowSequenceDiagram';
|
|
44
|
+
export type { WorkflowSequenceDiagramProps } from './components/WorkflowSequenceDiagram';
|
|
45
|
+
|
|
43
46
|
export { useSequenceLayout } from './hooks/useSequenceLayout';
|
|
44
47
|
export type {
|
|
45
48
|
SequenceEvent,
|