@principal-ai/principal-view-react 0.7.9 → 0.7.10

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.
@@ -0,0 +1,214 @@
1
+ import React from 'react';
2
+
3
+ interface SpanEvent {
4
+ time: number;
5
+ name: string;
6
+ attributes: Record<string, string | number | boolean>;
7
+ }
8
+
9
+ interface TestSpan {
10
+ id: string;
11
+ name: string;
12
+ startTime: number;
13
+ endTime?: number;
14
+ duration?: number;
15
+ attributes: Record<string, string | number | boolean>;
16
+ events: SpanEvent[];
17
+ status: 'OK' | 'ERROR';
18
+ errorMessage?: string;
19
+ }
20
+
21
+ export interface TestEventPanelProps {
22
+ spans: TestSpan[];
23
+ currentSpanIndex: number;
24
+ currentEventIndex: number;
25
+ highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
26
+ }
27
+
28
+ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
29
+ spans,
30
+ currentSpanIndex,
31
+ currentEventIndex,
32
+ highlightedPhase,
33
+ }) => {
34
+ const currentSpan = spans[currentSpanIndex];
35
+ const eventsUpToNow = currentSpan?.events.slice(0, currentEventIndex + 1) || [];
36
+
37
+ return (
38
+ <div
39
+ style={{
40
+ width: '100%',
41
+ height: '100%',
42
+ backgroundColor: '#1a1a1a',
43
+ color: '#ffffff',
44
+ padding: '20px',
45
+ fontFamily: 'monospace',
46
+ fontSize: '12px',
47
+ overflow: 'auto',
48
+ }}
49
+ >
50
+ <div style={{ fontWeight: 'bold', marginBottom: '15px', fontSize: '14px' }}>
51
+ 📊 Wide Event Pattern - Code Journey
52
+ </div>
53
+ <div style={{ fontSize: '10px', color: '#888', marginBottom: '15px' }}>
54
+ Test: {currentSpan?.name || 'Loading...'}
55
+ <br />
56
+ Events: {eventsUpToNow.length} / {currentSpan?.events.length || 0}
57
+ </div>
58
+ <div style={{ fontSize: '10px', color: '#a3a3a3', marginBottom: '20px', lineHeight: '1.5' }}>
59
+ Watch how execution flows through files:
60
+ <br />
61
+ <span style={{ color: '#60a5fa' }}>Blue = Test file</span>
62
+ {' • '}
63
+ <span style={{ color: '#4ade80' }}>Green → Code under test</span>
64
+ </div>
65
+
66
+ {currentSpan && (
67
+ <>
68
+ {/* Span Attributes (static context) */}
69
+ <div style={{ marginBottom: '20px' }}>
70
+ <div
71
+ style={{
72
+ color: '#60a5fa',
73
+ fontWeight: 'bold',
74
+ marginBottom: '8px',
75
+ fontSize: '11px',
76
+ }}
77
+ >
78
+ Span Context (Static)
79
+ </div>
80
+ <pre
81
+ style={{
82
+ background: '#0d0d0d',
83
+ padding: '10px',
84
+ borderRadius: '4px',
85
+ margin: 0,
86
+ fontSize: '10px',
87
+ lineHeight: '1.5',
88
+ }}
89
+ >
90
+ {JSON.stringify(
91
+ {
92
+ 'test.file': currentSpan.attributes['test.file'],
93
+ 'test.suite': currentSpan.attributes['test.suite'],
94
+ 'test.result': currentSpan.attributes['test.result'],
95
+ },
96
+ null,
97
+ 2
98
+ )}
99
+ </pre>
100
+ </div>
101
+
102
+ {/* Event Timeline (context mutations) */}
103
+ <div>
104
+ <div
105
+ style={{
106
+ color: '#4ade80',
107
+ fontWeight: 'bold',
108
+ marginBottom: '8px',
109
+ fontSize: '11px',
110
+ }}
111
+ >
112
+ Event Timeline (Context Mutations)
113
+ </div>
114
+ {eventsUpToNow.map((event, idx) => {
115
+ const filepath = event.attributes['code.filepath'] as string;
116
+ const lineno = event.attributes['code.lineno'] as number;
117
+ const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
118
+
119
+ // Determine which phase this event belongs to
120
+ const eventPhase = event.name.split('.')[0]; // 'setup', 'execution', 'assertion'
121
+ const isHighlighted = highlightedPhase === eventPhase;
122
+
123
+ return (
124
+ <div
125
+ key={idx}
126
+ style={{
127
+ marginBottom: '12px',
128
+ paddingBottom: '12px',
129
+ borderBottom: idx < eventsUpToNow.length - 1 ? '1px solid #333' : 'none',
130
+ opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
131
+ transition: 'opacity 0.2s ease',
132
+ transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
133
+ backgroundColor: isHighlighted ? '#1e293b' : 'transparent',
134
+ padding: isHighlighted ? '8px' : '0',
135
+ borderRadius: '4px',
136
+ }}
137
+ >
138
+ <div
139
+ style={{
140
+ display: 'flex',
141
+ justifyContent: 'space-between',
142
+ alignItems: 'center',
143
+ marginBottom: '4px',
144
+ }}
145
+ >
146
+ <div style={{ color: '#f59e0b', fontSize: '10px' }}>
147
+ {idx + 1}. {event.name}
148
+ </div>
149
+ {filepath && (
150
+ <div
151
+ style={{
152
+ fontSize: '9px',
153
+ color: isCodeUnderTest ? '#4ade80' : '#60a5fa',
154
+ fontFamily: 'monospace',
155
+ background: isCodeUnderTest ? '#064e3b' : '#1e3a8a',
156
+ padding: '2px 6px',
157
+ borderRadius: '3px',
158
+ }}
159
+ >
160
+ {isCodeUnderTest && '→ '}
161
+ {filepath}:{lineno}
162
+ </div>
163
+ )}
164
+ </div>
165
+ <pre
166
+ style={{
167
+ background: '#0d0d0d',
168
+ padding: '8px',
169
+ borderRadius: '4px',
170
+ margin: 0,
171
+ fontSize: '9px',
172
+ lineHeight: '1.4',
173
+ }}
174
+ >
175
+ {JSON.stringify(
176
+ Object.fromEntries(
177
+ Object.entries(event.attributes).filter(
178
+ ([key]) => key !== 'code.filepath' && key !== 'code.lineno'
179
+ )
180
+ ),
181
+ null,
182
+ 2
183
+ )}
184
+ </pre>
185
+ </div>
186
+ );
187
+ })}
188
+ </div>
189
+ </>
190
+ )}
191
+
192
+ <div
193
+ style={{
194
+ marginTop: '20px',
195
+ paddingTop: '15px',
196
+ borderTop: '1px solid #333',
197
+ fontSize: '10px',
198
+ color: '#888',
199
+ }}
200
+ >
201
+ <div style={{ marginBottom: '8px' }}>
202
+ <strong>Total tests:</strong> {spans.length}
203
+ </div>
204
+ <div style={{ marginBottom: '8px' }}>
205
+ <strong>Pattern:</strong> One span per test + event timeline
206
+ </div>
207
+ <div>
208
+ <strong>Status:</strong>{' '}
209
+ <span style={{ color: '#4ade80' }}>All Passed ✓</span>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ };
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';
@@ -6,6 +6,24 @@ 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 soft/lighter version with transparency
11
+ * @param hexColor - Hex color string (e.g., "#FF5733" or "#888")
12
+ * @param alpha - Transparency level (0-1), defaults to 0.12 for subtle backgrounds
13
+ * @returns rgba color string
14
+ */
15
+ function hexToSoftColor(hexColor: string, alpha = 0.12): 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
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
25
+ }
26
+
9
27
  export interface CustomNodeData extends Record<string, unknown> {
10
28
  name: string;
11
29
  typeDefinition: NodeTypeDefinition;
@@ -19,6 +37,8 @@ export interface CustomNodeData extends Record<string, unknown> {
19
37
  editable?: boolean;
20
38
  // Whether tooltips are enabled (defaults to true)
21
39
  tooltipsEnabled?: boolean;
40
+ // Whether this node is highlighted (e.g., during execution playback)
41
+ isHighlighted?: boolean;
22
42
  }
23
43
 
24
44
  /**
@@ -41,6 +61,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
41
61
  animationDuration = 1000,
42
62
  editable = false,
43
63
  tooltipsEnabled = true,
64
+ isHighlighted = false,
44
65
  } = nodeProps;
45
66
 
46
67
  // Extract OTEL info and description for tooltip
@@ -155,7 +176,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
155
176
  const getShapeStyles = () => {
156
177
  const baseStyles = {
157
178
  padding: '12px 16px',
158
- backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : 'white',
179
+ backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : hexToSoftColor(fillColor),
159
180
  color: '#000',
160
181
  border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
161
182
  fontSize: '12px',
@@ -171,7 +192,11 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
171
192
  alignItems: 'center',
172
193
  justifyContent: isGroup ? 'flex-start' : 'center',
173
194
  gap: '4px',
174
- boxShadow: selected ? `0 0 0 2px ${strokeColor}` : '0 2px 4px rgba(0,0,0,0.1)',
195
+ boxShadow: isHighlighted
196
+ ? `0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.5)`
197
+ : selected
198
+ ? `0 0 0 2px ${strokeColor}`
199
+ : '0 2px 4px rgba(0,0,0,0.1)',
175
200
  transition: 'box-shadow 0.2s ease',
176
201
  animationDuration: animationType ? `${animationDuration}ms` : undefined,
177
202
  boxSizing: 'border-box' as const,
@@ -253,7 +278,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
253
278
  }
254
279
  : {};
255
280
 
256
- // Hexagon inner fill styles (white background inset from border)
281
+ // Hexagon inner fill styles (soft color background inset from border)
257
282
  const hexagonInnerStyle: React.CSSProperties = isHexagon
258
283
  ? {
259
284
  position: 'absolute',
@@ -262,7 +287,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
262
287
  right: hexagonBorderWidth,
263
288
  bottom: hexagonBorderWidth,
264
289
  clipPath: hexagonClipPath,
265
- backgroundColor: 'white',
290
+ backgroundColor: hexToSoftColor(fillColor),
266
291
  color: '#000',
267
292
  display: 'flex',
268
293
  flexDirection: 'column',
@@ -295,7 +320,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
295
320
  }
296
321
  : {};
297
322
 
298
- // Diamond inner fill styles (white background inset from border)
323
+ // Diamond inner fill styles (soft color background inset from border)
299
324
  const diamondInnerStyle: React.CSSProperties = isDiamond
300
325
  ? {
301
326
  position: 'absolute',
@@ -304,7 +329,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
304
329
  right: diamondBorderWidth,
305
330
  bottom: diamondBorderWidth,
306
331
  clipPath: diamondClipPath,
307
- backgroundColor: 'white',
332
+ backgroundColor: hexToSoftColor(fillColor),
308
333
  color: '#000',
309
334
  display: 'flex',
310
335
  flexDirection: 'column',