@principal-ai/principal-view-react 0.7.9 → 0.7.11
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/GraphRenderer.d.ts +5 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +23 -7
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/LayerPanel.d.ts +31 -0
- package/dist/components/LayerPanel.d.ts.map +1 -0
- package/dist/components/LayerPanel.js +207 -0
- package/dist/components/LayerPanel.js.map +1 -0
- package/dist/components/TestEventPanel.d.ts +26 -0
- package/dist/components/TestEventPanel.d.ts.map +1 -0
- package/dist/components/TestEventPanel.js +131 -0
- package/dist/components/TestEventPanel.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +33 -7
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +2 -2
- package/src/components/GraphRenderer.tsx +36 -5
- package/src/components/TestEventPanel.tsx +287 -0
- package/src/index.ts +3 -0
- package/src/nodes/CustomNode.tsx +39 -6
- package/src/stories/MultiConfig.stories.tsx +1 -1
- package/src/stories/MultiDirectionalConnections.stories.tsx +0 -1
- package/src/stories/RealTestExecution.stories.tsx +280 -0
- package/src/stories/ValidatedExecution.stories.tsx +158 -0
- package/src/stories/data/graph-converter-test-execution.json +225 -0
- package/src/stories/data/graph-converter-validated-execution.json +58 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
3
|
+
import { HelpCircle } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface SpanEvent {
|
|
6
|
+
time: number;
|
|
7
|
+
name: string;
|
|
8
|
+
attributes: Record<string, string | number | boolean>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TestSpan {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
startTime: number;
|
|
15
|
+
endTime?: number;
|
|
16
|
+
duration?: number;
|
|
17
|
+
attributes: Record<string, string | number | boolean>;
|
|
18
|
+
events: SpanEvent[];
|
|
19
|
+
status: 'OK' | 'ERROR';
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TestEventPanelProps {
|
|
24
|
+
spans: TestSpan[];
|
|
25
|
+
currentSpanIndex: number;
|
|
26
|
+
currentEventIndex: number;
|
|
27
|
+
highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
31
|
+
spans,
|
|
32
|
+
currentSpanIndex,
|
|
33
|
+
currentEventIndex,
|
|
34
|
+
highlightedPhase,
|
|
35
|
+
}) => {
|
|
36
|
+
const { theme } = useTheme();
|
|
37
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
38
|
+
const currentSpan = spans[currentSpanIndex];
|
|
39
|
+
const eventsUpToNow = currentSpan?.events.slice(0, currentEventIndex + 1) || [];
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
width: '100%',
|
|
45
|
+
height: '100%',
|
|
46
|
+
backgroundColor: theme.colors.background,
|
|
47
|
+
color: theme.colors.text,
|
|
48
|
+
padding: '20px',
|
|
49
|
+
fontFamily: theme.fonts.monospace,
|
|
50
|
+
fontSize: '14px',
|
|
51
|
+
overflow: 'auto',
|
|
52
|
+
boxSizing: 'border-box',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '15px' }}>
|
|
56
|
+
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
|
57
|
+
Wide Event Pattern - Code Journey
|
|
58
|
+
</div>
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => setShowHelp(true)}
|
|
61
|
+
style={{
|
|
62
|
+
background: 'transparent',
|
|
63
|
+
border: 'none',
|
|
64
|
+
cursor: 'pointer',
|
|
65
|
+
padding: '4px',
|
|
66
|
+
display: 'flex',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
color: theme.colors.textMuted,
|
|
69
|
+
}}
|
|
70
|
+
onMouseEnter={(e) => {
|
|
71
|
+
e.currentTarget.style.color = theme.colors.text;
|
|
72
|
+
}}
|
|
73
|
+
onMouseLeave={(e) => {
|
|
74
|
+
e.currentTarget.style.color = theme.colors.textMuted;
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<HelpCircle size={20} />
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
|
|
81
|
+
Test: {currentSpan?.name || 'Loading...'}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Help Modal */}
|
|
85
|
+
{showHelp && (
|
|
86
|
+
<div
|
|
87
|
+
style={{
|
|
88
|
+
position: 'fixed',
|
|
89
|
+
top: 0,
|
|
90
|
+
left: 0,
|
|
91
|
+
right: 0,
|
|
92
|
+
bottom: 0,
|
|
93
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
94
|
+
display: 'flex',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
zIndex: 9999,
|
|
98
|
+
}}
|
|
99
|
+
onClick={() => setShowHelp(false)}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
backgroundColor: theme.colors.background,
|
|
104
|
+
color: theme.colors.text,
|
|
105
|
+
padding: '24px',
|
|
106
|
+
borderRadius: '8px',
|
|
107
|
+
maxWidth: '600px',
|
|
108
|
+
border: `1px solid ${theme.colors.border}`,
|
|
109
|
+
}}
|
|
110
|
+
onClick={(e) => e.stopPropagation()}
|
|
111
|
+
>
|
|
112
|
+
<div style={{ fontWeight: 'bold', fontSize: '18px', marginBottom: '16px' }}>
|
|
113
|
+
How to Read This Panel
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
|
|
116
|
+
<p style={{ marginBottom: '12px' }}>
|
|
117
|
+
<strong>Watch how execution flows through files:</strong>
|
|
118
|
+
</p>
|
|
119
|
+
<ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
|
|
120
|
+
<li style={{ marginBottom: '8px' }}>
|
|
121
|
+
<span style={{ color: '#60a5fa' }}>Blue = Test file</span>
|
|
122
|
+
</li>
|
|
123
|
+
<li>
|
|
124
|
+
<span style={{ color: '#4ade80' }}>Green → Code under test</span>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
<p style={{ marginBottom: '12px' }}>
|
|
128
|
+
<strong>Span Context (Static)</strong>
|
|
129
|
+
</p>
|
|
130
|
+
<pre
|
|
131
|
+
style={{
|
|
132
|
+
background: theme.colors.surface,
|
|
133
|
+
padding: '12px',
|
|
134
|
+
borderRadius: '4px',
|
|
135
|
+
fontSize: '13px',
|
|
136
|
+
overflow: 'auto',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{`{
|
|
140
|
+
"test.file": "GraphConverter.test.ts",
|
|
141
|
+
"test.suite": "GraphConverter",
|
|
142
|
+
"test.result": "pass"
|
|
143
|
+
}`}
|
|
144
|
+
</pre>
|
|
145
|
+
</div>
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => setShowHelp(false)}
|
|
148
|
+
style={{
|
|
149
|
+
padding: '8px 16px',
|
|
150
|
+
backgroundColor: theme.colors.primary,
|
|
151
|
+
color: theme.colors.background,
|
|
152
|
+
border: 'none',
|
|
153
|
+
borderRadius: '4px',
|
|
154
|
+
cursor: 'pointer',
|
|
155
|
+
fontSize: '14px',
|
|
156
|
+
fontWeight: 500,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
Got it
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{currentSpan && (
|
|
166
|
+
<>
|
|
167
|
+
{/* Event Timeline (context mutations) */}
|
|
168
|
+
<div>
|
|
169
|
+
<div
|
|
170
|
+
style={{
|
|
171
|
+
color: '#4ade80',
|
|
172
|
+
fontWeight: 'bold',
|
|
173
|
+
marginBottom: '8px',
|
|
174
|
+
fontSize: '15px',
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
Event Timeline (Context Mutations)
|
|
178
|
+
</div>
|
|
179
|
+
{eventsUpToNow.map((event, idx) => {
|
|
180
|
+
const filepath = event.attributes['code.filepath'] as string;
|
|
181
|
+
const lineno = event.attributes['code.lineno'] as number;
|
|
182
|
+
const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
|
|
183
|
+
|
|
184
|
+
// Determine which phase this event belongs to
|
|
185
|
+
const eventPhase = event.name.split('.')[0]; // 'setup', 'execution', 'assertion'
|
|
186
|
+
const isHighlighted = highlightedPhase === eventPhase;
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
key={idx}
|
|
191
|
+
style={{
|
|
192
|
+
marginBottom: '12px',
|
|
193
|
+
paddingBottom: '12px',
|
|
194
|
+
borderBottom: idx < eventsUpToNow.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
|
|
195
|
+
opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
|
|
196
|
+
transition: 'opacity 0.2s ease',
|
|
197
|
+
transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
|
|
198
|
+
backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
|
|
199
|
+
padding: isHighlighted ? '8px' : '0',
|
|
200
|
+
borderRadius: '4px',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
display: 'flex',
|
|
206
|
+
justifyContent: 'space-between',
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
marginBottom: '4px',
|
|
209
|
+
gap: '8px',
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div style={{ color: '#f59e0b', fontSize: '13px', flexShrink: 0 }}>
|
|
213
|
+
{idx + 1}. {event.name}
|
|
214
|
+
</div>
|
|
215
|
+
{filepath && (
|
|
216
|
+
<div
|
|
217
|
+
style={{
|
|
218
|
+
fontSize: '12px',
|
|
219
|
+
color: isCodeUnderTest ? '#4ade80' : '#60a5fa',
|
|
220
|
+
fontFamily: 'monospace',
|
|
221
|
+
background: isCodeUnderTest ? '#064e3b' : '#1e3a8a',
|
|
222
|
+
padding: '2px 6px',
|
|
223
|
+
borderRadius: '3px',
|
|
224
|
+
flexShrink: 1,
|
|
225
|
+
minWidth: 0,
|
|
226
|
+
overflow: 'hidden',
|
|
227
|
+
textOverflow: 'ellipsis',
|
|
228
|
+
whiteSpace: 'nowrap',
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{isCodeUnderTest && '→ '}
|
|
232
|
+
{filepath}:{lineno}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
<pre
|
|
237
|
+
style={{
|
|
238
|
+
background: theme.colors.surface,
|
|
239
|
+
padding: '8px',
|
|
240
|
+
borderRadius: '4px',
|
|
241
|
+
margin: 0,
|
|
242
|
+
fontSize: '12px',
|
|
243
|
+
lineHeight: '1.4',
|
|
244
|
+
overflow: 'auto',
|
|
245
|
+
maxWidth: '100%',
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{JSON.stringify(
|
|
249
|
+
Object.fromEntries(
|
|
250
|
+
Object.entries(event.attributes).filter(
|
|
251
|
+
([key]) => key !== 'code.filepath' && key !== 'code.lineno'
|
|
252
|
+
)
|
|
253
|
+
),
|
|
254
|
+
null,
|
|
255
|
+
2
|
|
256
|
+
)}
|
|
257
|
+
</pre>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
</div>
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
<div
|
|
266
|
+
style={{
|
|
267
|
+
marginTop: '20px',
|
|
268
|
+
paddingTop: '15px',
|
|
269
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
270
|
+
fontSize: '13px',
|
|
271
|
+
color: theme.colors.textMuted,
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
<div style={{ marginBottom: '8px' }}>
|
|
275
|
+
<strong>Total tests:</strong> {spans.length}
|
|
276
|
+
</div>
|
|
277
|
+
<div style={{ marginBottom: '8px' }}>
|
|
278
|
+
<strong>Pattern:</strong> One span per test + event timeline
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<strong>Status:</strong>{' '}
|
|
282
|
+
<span style={{ color: '#4ade80' }}>All Passed ✓</span>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,9 @@ export type { NodeInfoPanelProps } from './components/NodeInfoPanel';
|
|
|
52
52
|
export { ConfigurationSelector } from './components/ConfigurationSelector';
|
|
53
53
|
export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
|
|
54
54
|
|
|
55
|
+
export { TestEventPanel } from './components/TestEventPanel';
|
|
56
|
+
export type { TestEventPanelProps } from './components/TestEventPanel';
|
|
57
|
+
|
|
55
58
|
// Export node/edge renderers
|
|
56
59
|
export { GenericNode } from './nodes/GenericNode';
|
|
57
60
|
export type { GenericNodeProps } from './nodes/GenericNode';
|
package/src/nodes/CustomNode.tsx
CHANGED
|
@@ -6,6 +6,32 @@ import { resolveIcon } from '../utils/iconResolver';
|
|
|
6
6
|
import { NodeTooltip } from '../components/NodeTooltip';
|
|
7
7
|
import type { OtelInfo } from '../components/NodeTooltip';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Converts a hex color to a lighter/tinted version (opaque, not transparent)
|
|
11
|
+
* @param hexColor - Hex color string (e.g., "#FF5733" or "#888")
|
|
12
|
+
* @param lightness - How much to lighten (0-1), defaults to 0.88 (88% white mixed in)
|
|
13
|
+
* @returns hex color string
|
|
14
|
+
*/
|
|
15
|
+
function hexToLightColor(hexColor: string, lightness = 0.88): string {
|
|
16
|
+
// Remove # if present
|
|
17
|
+
const hex = hexColor.replace('#', '');
|
|
18
|
+
|
|
19
|
+
// Parse hex to RGB
|
|
20
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
21
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
22
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
23
|
+
|
|
24
|
+
// Mix with white based on lightness factor
|
|
25
|
+
// lightness of 0.88 means 88% white + 12% original color
|
|
26
|
+
const newR = Math.round(r + (255 - r) * lightness);
|
|
27
|
+
const newG = Math.round(g + (255 - g) * lightness);
|
|
28
|
+
const newB = Math.round(b + (255 - b) * lightness);
|
|
29
|
+
|
|
30
|
+
// Convert back to hex
|
|
31
|
+
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
|
32
|
+
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
export interface CustomNodeData extends Record<string, unknown> {
|
|
10
36
|
name: string;
|
|
11
37
|
typeDefinition: NodeTypeDefinition;
|
|
@@ -19,6 +45,8 @@ export interface CustomNodeData extends Record<string, unknown> {
|
|
|
19
45
|
editable?: boolean;
|
|
20
46
|
// Whether tooltips are enabled (defaults to true)
|
|
21
47
|
tooltipsEnabled?: boolean;
|
|
48
|
+
// Whether this node is highlighted (e.g., during execution playback)
|
|
49
|
+
isHighlighted?: boolean;
|
|
22
50
|
}
|
|
23
51
|
|
|
24
52
|
/**
|
|
@@ -41,6 +69,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
41
69
|
animationDuration = 1000,
|
|
42
70
|
editable = false,
|
|
43
71
|
tooltipsEnabled = true,
|
|
72
|
+
isHighlighted = false,
|
|
44
73
|
} = nodeProps;
|
|
45
74
|
|
|
46
75
|
// Extract OTEL info and description for tooltip
|
|
@@ -155,7 +184,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
155
184
|
const getShapeStyles = () => {
|
|
156
185
|
const baseStyles = {
|
|
157
186
|
padding: '12px 16px',
|
|
158
|
-
backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' :
|
|
187
|
+
backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : hexToLightColor(fillColor),
|
|
159
188
|
color: '#000',
|
|
160
189
|
border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
|
|
161
190
|
fontSize: '12px',
|
|
@@ -171,7 +200,11 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
171
200
|
alignItems: 'center',
|
|
172
201
|
justifyContent: isGroup ? 'flex-start' : 'center',
|
|
173
202
|
gap: '4px',
|
|
174
|
-
boxShadow:
|
|
203
|
+
boxShadow: isHighlighted
|
|
204
|
+
? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
|
|
205
|
+
: selected
|
|
206
|
+
? `0 0 0 2px ${strokeColor}`
|
|
207
|
+
: '0 2px 4px rgba(0,0,0,0.1)',
|
|
175
208
|
transition: 'box-shadow 0.2s ease',
|
|
176
209
|
animationDuration: animationType ? `${animationDuration}ms` : undefined,
|
|
177
210
|
boxSizing: 'border-box' as const,
|
|
@@ -253,7 +286,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
253
286
|
}
|
|
254
287
|
: {};
|
|
255
288
|
|
|
256
|
-
// Hexagon inner fill styles (
|
|
289
|
+
// Hexagon inner fill styles (light color background inset from border)
|
|
257
290
|
const hexagonInnerStyle: React.CSSProperties = isHexagon
|
|
258
291
|
? {
|
|
259
292
|
position: 'absolute',
|
|
@@ -262,7 +295,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
262
295
|
right: hexagonBorderWidth,
|
|
263
296
|
bottom: hexagonBorderWidth,
|
|
264
297
|
clipPath: hexagonClipPath,
|
|
265
|
-
backgroundColor:
|
|
298
|
+
backgroundColor: hexToLightColor(fillColor),
|
|
266
299
|
color: '#000',
|
|
267
300
|
display: 'flex',
|
|
268
301
|
flexDirection: 'column',
|
|
@@ -295,7 +328,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
295
328
|
}
|
|
296
329
|
: {};
|
|
297
330
|
|
|
298
|
-
// Diamond inner fill styles (
|
|
331
|
+
// Diamond inner fill styles (light color background inset from border)
|
|
299
332
|
const diamondInnerStyle: React.CSSProperties = isDiamond
|
|
300
333
|
? {
|
|
301
334
|
position: 'absolute',
|
|
@@ -304,7 +337,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
304
337
|
right: diamondBorderWidth,
|
|
305
338
|
bottom: diamondBorderWidth,
|
|
306
339
|
clipPath: diamondClipPath,
|
|
307
|
-
backgroundColor:
|
|
340
|
+
backgroundColor: hexToLightColor(fillColor),
|
|
308
341
|
color: '#000',
|
|
309
342
|
display: 'flex',
|
|
310
343
|
flexDirection: 'column',
|
|
@@ -411,7 +411,7 @@ function MultiConfigDemo() {
|
|
|
411
411
|
|
|
412
412
|
{/* Graph visualization */}
|
|
413
413
|
<div style={{ flex: 1 }}>
|
|
414
|
-
<GraphRenderer canvas={selectedConfig.canvas}
|
|
414
|
+
<GraphRenderer canvas={selectedConfig.canvas} showControls showBackground />
|
|
415
415
|
</div>
|
|
416
416
|
</div>
|
|
417
417
|
);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { GraphRenderer } from '../components/GraphRenderer';
|
|
4
|
+
import { TestEventPanel } from '../components/TestEventPanel';
|
|
5
|
+
import type { ExtendedCanvas, GraphEvent } from '@principal-ai/principal-view-core';
|
|
6
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
7
|
+
import testSpans from './data/graph-converter-test-execution.json';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Features/Real Test Execution',
|
|
11
|
+
component: GraphRenderer,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component:
|
|
17
|
+
'Visualizes REAL test execution data from instrumented Bun tests using the "wide event" pattern. Shows actual spans with file/line information collected from running GraphConverter.test.ts. Hover over graph nodes to highlight related events in the panel.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
tags: ['autodocs'],
|
|
22
|
+
decorators: [
|
|
23
|
+
(Story) => (
|
|
24
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
25
|
+
<div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
|
|
26
|
+
<Story />
|
|
27
|
+
</div>
|
|
28
|
+
</ThemeProvider>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
} satisfies Meta<typeof GraphRenderer>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Test Execution Flow Canvas
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
const testExecutionCanvas: ExtendedCanvas = {
|
|
41
|
+
nodes: [
|
|
42
|
+
// Test Suite
|
|
43
|
+
{
|
|
44
|
+
id: 'test-suite',
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: 'GraphConverter Test Suite',
|
|
47
|
+
x: -100,
|
|
48
|
+
y: -100,
|
|
49
|
+
width: 240,
|
|
50
|
+
height: 80,
|
|
51
|
+
pv: {
|
|
52
|
+
nodeType: 'test-suite',
|
|
53
|
+
name: 'Test Suite',
|
|
54
|
+
description: 'Collection of GraphConverter tests',
|
|
55
|
+
shape: 'rectangle',
|
|
56
|
+
fill: '#3b82f6',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Test Phase Nodes
|
|
61
|
+
{
|
|
62
|
+
id: 'setup-phase',
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: 'Setup',
|
|
65
|
+
x: -250,
|
|
66
|
+
y: 50,
|
|
67
|
+
width: 120,
|
|
68
|
+
height: 80,
|
|
69
|
+
pv: {
|
|
70
|
+
nodeType: 'test-phase',
|
|
71
|
+
name: 'Setup Phase',
|
|
72
|
+
description: 'Test data preparation',
|
|
73
|
+
shape: 'hexagon',
|
|
74
|
+
fill: '#10b981',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'execution-phase',
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: 'Execution',
|
|
81
|
+
x: -80,
|
|
82
|
+
y: 50,
|
|
83
|
+
width: 120,
|
|
84
|
+
height: 80,
|
|
85
|
+
pv: {
|
|
86
|
+
nodeType: 'test-phase',
|
|
87
|
+
name: 'Execution Phase',
|
|
88
|
+
description: 'Code under test runs',
|
|
89
|
+
shape: 'hexagon',
|
|
90
|
+
fill: '#f59e0b',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'assertion-phase',
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: 'Assertion',
|
|
97
|
+
x: 90,
|
|
98
|
+
y: 50,
|
|
99
|
+
width: 120,
|
|
100
|
+
height: 80,
|
|
101
|
+
pv: {
|
|
102
|
+
nodeType: 'test-phase',
|
|
103
|
+
name: 'Assertion Phase',
|
|
104
|
+
description: 'Verify results',
|
|
105
|
+
shape: 'hexagon',
|
|
106
|
+
fill: '#8b5cf6',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Result Node
|
|
111
|
+
{
|
|
112
|
+
id: 'test-result',
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: 'Test Result',
|
|
115
|
+
x: -100,
|
|
116
|
+
y: 200,
|
|
117
|
+
width: 240,
|
|
118
|
+
height: 80,
|
|
119
|
+
pv: {
|
|
120
|
+
nodeType: 'result',
|
|
121
|
+
name: 'Test Result',
|
|
122
|
+
description: 'Pass/Fail outcome',
|
|
123
|
+
shape: 'rectangle',
|
|
124
|
+
fill: '#10b981',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
edges: [
|
|
129
|
+
{
|
|
130
|
+
id: 'suite-to-setup',
|
|
131
|
+
fromNode: 'test-suite',
|
|
132
|
+
toNode: 'setup-phase',
|
|
133
|
+
fromSide: 'bottom',
|
|
134
|
+
toSide: 'top',
|
|
135
|
+
label: 'start test',
|
|
136
|
+
pv: {
|
|
137
|
+
edgeType: 'flow',
|
|
138
|
+
style: 'solid',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'setup-to-execution',
|
|
143
|
+
fromNode: 'setup-phase',
|
|
144
|
+
toNode: 'execution-phase',
|
|
145
|
+
fromSide: 'right',
|
|
146
|
+
toSide: 'left',
|
|
147
|
+
label: 'data ready',
|
|
148
|
+
pv: {
|
|
149
|
+
edgeType: 'flow',
|
|
150
|
+
style: 'solid',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'execution-to-assertion',
|
|
155
|
+
fromNode: 'execution-phase',
|
|
156
|
+
toNode: 'assertion-phase',
|
|
157
|
+
fromSide: 'right',
|
|
158
|
+
toSide: 'left',
|
|
159
|
+
label: 'got result',
|
|
160
|
+
pv: {
|
|
161
|
+
edgeType: 'flow',
|
|
162
|
+
style: 'solid',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'assertion-to-result',
|
|
167
|
+
fromNode: 'assertion-phase',
|
|
168
|
+
toNode: 'test-result',
|
|
169
|
+
fromSide: 'bottom',
|
|
170
|
+
toSide: 'top',
|
|
171
|
+
label: 'complete',
|
|
172
|
+
pv: {
|
|
173
|
+
edgeType: 'flow',
|
|
174
|
+
style: 'solid',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
pv: {
|
|
179
|
+
version: '1.0.0',
|
|
180
|
+
name: 'Test Execution Flow',
|
|
181
|
+
description: 'Visualizes the flow of test execution through phases',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Interactive Story (No Animation)
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
const AnimatedTestExecution = () => {
|
|
190
|
+
const [events] = useState<GraphEvent[]>([]);
|
|
191
|
+
const [currentSpanIndex] = useState(0);
|
|
192
|
+
// Show all events by default - set to a large number
|
|
193
|
+
const [currentEventIndex] = useState(999);
|
|
194
|
+
const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
|
|
198
|
+
{/* Graph Visualization - Left Side */}
|
|
199
|
+
<div
|
|
200
|
+
style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}
|
|
201
|
+
onMouseLeave={() => setHighlightedPhase(undefined)}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
style={{ width: '100%', height: '100%' }}
|
|
205
|
+
onMouseOver={(e) => {
|
|
206
|
+
// Check if hovering over a phase node
|
|
207
|
+
const target = e.target as HTMLElement;
|
|
208
|
+
const textContent = target.textContent;
|
|
209
|
+
if (textContent === 'Setup') setHighlightedPhase('setup');
|
|
210
|
+
else if (textContent === 'Execution') setHighlightedPhase('execution');
|
|
211
|
+
else if (textContent === 'Assertion') setHighlightedPhase('assertion');
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
<GraphRenderer
|
|
215
|
+
canvas={testExecutionCanvas}
|
|
216
|
+
showControls={true}
|
|
217
|
+
events={events}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* Event Panel - Right Side */}
|
|
223
|
+
<div style={{ flex: '0 0 40%', height: '100%', borderLeft: `1px solid #333`, overflow: 'hidden' }}>
|
|
224
|
+
<TestEventPanel
|
|
225
|
+
spans={testSpans as any}
|
|
226
|
+
currentSpanIndex={currentSpanIndex}
|
|
227
|
+
currentEventIndex={currentEventIndex}
|
|
228
|
+
highlightedPhase={highlightedPhase}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Interactive visualization of real test execution data using the "wide event" pattern.
|
|
237
|
+
*
|
|
238
|
+
* This demonstrates the key concept from loggingsucks.com:
|
|
239
|
+
* - ONE comprehensive span per test (not multiple child spans)
|
|
240
|
+
* - Events show the narrative of what happened during execution
|
|
241
|
+
* - Context accumulates through event attributes with file/line information
|
|
242
|
+
* - Easy to search by test.name to get full execution story
|
|
243
|
+
*
|
|
244
|
+
* **Interaction:**
|
|
245
|
+
* - Hover over graph nodes (Setup, Execution, Assertion) to highlight related events
|
|
246
|
+
* - Watch the code journey: blue = test file, green = code under test
|
|
247
|
+
* - All events are shown immediately for easy review
|
|
248
|
+
*/
|
|
249
|
+
export const Animated: Story = {
|
|
250
|
+
render: () => <AnimatedTestExecution />,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Static view of the test execution flow showing phases.
|
|
255
|
+
*/
|
|
256
|
+
export const StaticView: Story = {
|
|
257
|
+
args: {
|
|
258
|
+
canvas: testExecutionCanvas,
|
|
259
|
+
showControls: true,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Event panel component showing test execution narrative with file/line information.
|
|
265
|
+
*
|
|
266
|
+
* Shows how events accumulate context as tests execute, with automatic file/line
|
|
267
|
+
* capture from stack traces and manual override for code under test.
|
|
268
|
+
*/
|
|
269
|
+
export const EventPanelOnly: StoryObj = {
|
|
270
|
+
render: () => (
|
|
271
|
+
<div style={{ width: '600px', height: '100vh' }}>
|
|
272
|
+
<TestEventPanel
|
|
273
|
+
spans={testSpans as any}
|
|
274
|
+
currentSpanIndex={0}
|
|
275
|
+
currentEventIndex={999} // Show all events
|
|
276
|
+
highlightedPhase={undefined}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
),
|
|
280
|
+
};
|