@principal-ai/principal-view-react 0.14.21 → 0.14.23

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.
Files changed (57) hide show
  1. package/dist/components/GraphRenderer.d.ts.map +1 -1
  2. package/dist/components/GraphRenderer.js +23 -10
  3. package/dist/components/GraphRenderer.js.map +1 -1
  4. package/dist/components/state-view/PipelineView.d.ts +13 -0
  5. package/dist/components/state-view/PipelineView.d.ts.map +1 -0
  6. package/dist/components/state-view/PipelineView.js +195 -0
  7. package/dist/components/state-view/PipelineView.js.map +1 -0
  8. package/dist/components/state-view/index.d.ts +14 -0
  9. package/dist/components/state-view/index.d.ts.map +1 -0
  10. package/dist/components/state-view/index.js +12 -0
  11. package/dist/components/state-view/index.js.map +1 -0
  12. package/dist/components/state-view/types.d.ts +188 -0
  13. package/dist/components/state-view/types.d.ts.map +1 -0
  14. package/dist/components/state-view/types.js +10 -0
  15. package/dist/components/state-view/types.js.map +1 -0
  16. package/dist/components/state-view/useStateView.d.ts +32 -0
  17. package/dist/components/state-view/useStateView.d.ts.map +1 -0
  18. package/dist/components/state-view/useStateView.js +129 -0
  19. package/dist/components/state-view/useStateView.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/nodes/CustomNode.js +8 -8
  25. package/dist/nodes/CustomNode.js.map +1 -1
  26. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -1
  27. package/dist/nodes/otel/OtelBoundaryNode.js +5 -3
  28. package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -1
  29. package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -1
  30. package/dist/nodes/otel/OtelEventNode.js +5 -3
  31. package/dist/nodes/otel/OtelEventNode.js.map +1 -1
  32. package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -1
  33. package/dist/nodes/otel/OtelResourceNode.js +5 -3
  34. package/dist/nodes/otel/OtelResourceNode.js.map +1 -1
  35. package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -1
  36. package/dist/nodes/otel/OtelScopeNode.js +5 -3
  37. package/dist/nodes/otel/OtelScopeNode.js.map +1 -1
  38. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -1
  39. package/dist/nodes/otel/OtelSpanConventionNode.js +5 -3
  40. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/components/GraphRenderer.tsx +24 -10
  43. package/src/components/state-view/PipelineView.tsx +347 -0
  44. package/src/components/state-view/index.ts +14 -0
  45. package/src/components/state-view/types.ts +261 -0
  46. package/src/components/state-view/useStateView.ts +205 -0
  47. package/src/index.ts +36 -0
  48. package/src/nodes/CustomNode.tsx +8 -8
  49. package/src/nodes/otel/OtelBoundaryNode.tsx +5 -3
  50. package/src/nodes/otel/OtelEventNode.tsx +5 -3
  51. package/src/nodes/otel/OtelResourceNode.tsx +5 -3
  52. package/src/nodes/otel/OtelScopeNode.tsx +5 -3
  53. package/src/nodes/otel/OtelSpanConventionNode.tsx +5 -4
  54. package/src/stories/CanvasEdgeTypes.stories.tsx +23 -27
  55. package/src/stories/GraphRenderer.stories.tsx +144 -200
  56. package/src/stories/StateView.stories.tsx +417 -0
  57. package/src/stories/__traces__/test-run.canvas.json +27 -30
@@ -0,0 +1,205 @@
1
+ /**
2
+ * useStateView Hook
3
+ *
4
+ * Manages state accumulation from events and provides replay controls.
5
+ */
6
+
7
+ import { useState, useEffect, useRef, useCallback } from 'react';
8
+ import type {
9
+ StateEvent,
10
+ EventSource,
11
+ StateReducer,
12
+ StateDiff,
13
+ ReplayControls,
14
+ ActiveAnimation,
15
+ TransitionDefinition,
16
+ } from './types';
17
+
18
+ export interface UseStateViewOptions<TState, TEvent extends StateEvent> {
19
+ /** Initial state */
20
+ initialState: TState;
21
+ /** Reducer to apply events */
22
+ reducer: StateReducer<TState, TEvent>;
23
+ /** Event source (live or replay) */
24
+ eventSource: EventSource<TEvent>;
25
+ /** Transition definitions for animations */
26
+ transitions?: TransitionDefinition[];
27
+ /** Callback when state changes */
28
+ onStateChange?: (diff: StateDiff<TState>) => void;
29
+ }
30
+
31
+ export interface UseStateViewResult<TState> {
32
+ /** Current state */
33
+ state: TState;
34
+ /** Active animations */
35
+ animations: ActiveAnimation[];
36
+ /** Whether we're in replay mode */
37
+ isReplay: boolean;
38
+ /** Replay controls (if in replay mode) */
39
+ replayControls?: ReplayControls;
40
+ /** Reset state to initial */
41
+ reset: () => void;
42
+ }
43
+
44
+ /**
45
+ * Get changed paths between two objects (shallow comparison)
46
+ */
47
+ function getChangedPaths(prev: unknown, next: unknown, prefix = ''): string[] {
48
+ const paths: string[] = [];
49
+
50
+ if (prev === next) return paths;
51
+
52
+ if (
53
+ typeof prev !== 'object' ||
54
+ typeof next !== 'object' ||
55
+ prev === null ||
56
+ next === null
57
+ ) {
58
+ return prefix ? [prefix] : [];
59
+ }
60
+
61
+ const prevObj = prev as Record<string, unknown>;
62
+ const nextObj = next as Record<string, unknown>;
63
+
64
+ const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
65
+
66
+ for (const key of allKeys) {
67
+ const path = prefix ? `${prefix}.${key}` : key;
68
+ if (prevObj[key] !== nextObj[key]) {
69
+ paths.push(path);
70
+ }
71
+ }
72
+
73
+ return paths;
74
+ }
75
+
76
+ /**
77
+ * Check if a transition should fire based on state change
78
+ */
79
+ function shouldTriggerTransition(
80
+ transition: TransitionDefinition,
81
+ prev: unknown,
82
+ next: unknown,
83
+ changedPaths: string[]
84
+ ): boolean {
85
+ // Check if the watched path changed
86
+ if (!changedPaths.some((p) => p.startsWith(transition.watch))) {
87
+ return false;
88
+ }
89
+
90
+ const prevValue = getValueAtPath(prev, transition.watch);
91
+ const nextValue = getValueAtPath(next, transition.watch);
92
+
93
+ switch (transition.condition) {
94
+ case 'changed':
95
+ return prevValue !== nextValue;
96
+ case 'increased':
97
+ return typeof nextValue === 'number' && typeof prevValue === 'number' && nextValue > prevValue;
98
+ case 'decreased':
99
+ return typeof nextValue === 'number' && typeof prevValue === 'number' && nextValue < prevValue;
100
+ case 'added':
101
+ return prevValue === undefined && nextValue !== undefined;
102
+ case 'removed':
103
+ return prevValue !== undefined && nextValue === undefined;
104
+ default:
105
+ return false;
106
+ }
107
+ }
108
+
109
+ function getValueAtPath(obj: unknown, path: string): unknown {
110
+ const parts = path.split('.');
111
+ let current = obj;
112
+ for (const part of parts) {
113
+ if (current === null || current === undefined) return undefined;
114
+ current = (current as Record<string, unknown>)[part];
115
+ }
116
+ return current;
117
+ }
118
+
119
+ export function useStateView<TState, TEvent extends StateEvent>({
120
+ initialState,
121
+ reducer,
122
+ eventSource,
123
+ transitions = [],
124
+ onStateChange,
125
+ }: UseStateViewOptions<TState, TEvent>): UseStateViewResult<TState> {
126
+ const [state, setState] = useState<TState>(initialState);
127
+ const [animations, setAnimations] = useState<ActiveAnimation[]>([]);
128
+ const prevStateRef = useRef<TState>(initialState);
129
+ const animationIdRef = useRef(0);
130
+
131
+ const triggerAnimation = useCallback(
132
+ (transition: TransitionDefinition, _event: StateEvent) => {
133
+ const id = `anim-${++animationIdRef.current}`;
134
+ const duration = transition.duration ?? 500;
135
+
136
+ const animation: ActiveAnimation = {
137
+ id,
138
+ type: transition.animate,
139
+ target: transition.target ?? transition.watch,
140
+ startTime: Date.now(),
141
+ duration,
142
+ params: transition.params,
143
+ };
144
+
145
+ setAnimations((prev) => [...prev, animation]);
146
+
147
+ // Auto-remove after duration
148
+ setTimeout(() => {
149
+ setAnimations((prev) => prev.filter((a) => a.id !== id));
150
+ }, duration);
151
+ },
152
+ []
153
+ );
154
+
155
+ const handleEvent = useCallback(
156
+ (event: TEvent) => {
157
+ setState((prevState) => {
158
+ const nextState = reducer(prevState, event);
159
+ const changedPaths = getChangedPaths(prevState, nextState);
160
+
161
+ // Trigger animations based on transitions
162
+ for (const transition of transitions) {
163
+ if (shouldTriggerTransition(transition, prevState, nextState, changedPaths)) {
164
+ triggerAnimation(transition, event);
165
+ }
166
+ }
167
+
168
+ // Notify listener
169
+ if (onStateChange && changedPaths.length > 0) {
170
+ onStateChange({
171
+ prev: prevState,
172
+ next: nextState,
173
+ changedPaths,
174
+ event,
175
+ });
176
+ }
177
+
178
+ prevStateRef.current = nextState;
179
+ return nextState;
180
+ });
181
+ },
182
+ [reducer, transitions, onStateChange, triggerAnimation]
183
+ );
184
+
185
+ // Subscribe to event source
186
+ useEffect(() => {
187
+ const unsubscribe = eventSource.subscribe(handleEvent);
188
+ return unsubscribe;
189
+ }, [eventSource, handleEvent]);
190
+
191
+ const reset = useCallback(() => {
192
+ setState(initialState);
193
+ setAnimations([]);
194
+ prevStateRef.current = initialState;
195
+ }, [initialState]);
196
+
197
+ const isReplay = eventSource.mode === 'replay';
198
+
199
+ return {
200
+ state,
201
+ animations,
202
+ isReplay,
203
+ reset,
204
+ };
205
+ }
package/src/index.ts CHANGED
@@ -164,3 +164,39 @@ export type {
164
164
  SourceLinkProps,
165
165
  TimeRangeSelectorProps,
166
166
  } from './components/dashboard';
167
+
168
+ // State View components for event-driven state visualization
169
+ export { PipelineView, useStateView } from './components/state-view';
170
+ export type {
171
+ // Core types
172
+ StateEvent,
173
+ EventSource,
174
+ StateReducer,
175
+ StateDiff,
176
+ TransitionDefinition,
177
+ ActiveAnimation,
178
+ AnimationType,
179
+ ReplayControls,
180
+ StateViewDefinition,
181
+ // Pipeline view types
182
+ PipelineState,
183
+ PipelineStage,
184
+ PipelineRepoState,
185
+ PipelineEvent,
186
+ PipelineEventType,
187
+ // Activity view types
188
+ ActivityState,
189
+ RoomState,
190
+ ActivityEvent,
191
+ ActivityEventType,
192
+ // Quota view types
193
+ QuotaDistributionState,
194
+ UserQuotaState,
195
+ QuotaEvent,
196
+ QuotaEventType,
197
+ // Hook types
198
+ UseStateViewOptions,
199
+ UseStateViewResult,
200
+ // Component props
201
+ PipelineViewProps,
202
+ } from './components/state-view';
@@ -442,8 +442,8 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
442
442
  }
443
443
 
444
444
  // Color Contract:
445
- // - scopeColor: Used as BORDER color (from library.yaml owned-scopes)
446
- // - spanColor: Used as FILL color (from .spans.canvas based on workflow context)
445
+ // - scopeColor: Used as FILL/background color (from library.yaml scopes)
446
+ // - spanColor: Used as BORDER color in workflow context (from .spans.canvas)
447
447
  // - For non-event canvases, falls back to legacy behavior (node color or type color)
448
448
 
449
449
  // Get colors from node data (injected by GraphRenderer)
@@ -458,15 +458,15 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
458
458
  const stateColor =
459
459
  state && (nodeDataStates?.[state]?.color || typeDefinition.states?.[state]?.color);
460
460
 
461
- // Fill color priority: state color > span color > node data color > type definition color > default
462
- // spanColor is the new primary source for fill (from .spans.canvas)
463
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#888';
461
+ // Fill color priority: state color > node data color > scope color > type definition color > default
462
+ // scopeColor provides background color from the scope definition in library.yaml
463
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#888';
464
464
  const fillColor = stateColor || baseFillColor;
465
465
 
466
- // Stroke/border color priority: explicit stroke > scope color > fill color
467
- // scopeColor is now the primary source for border (from library.yaml owned-scopes)
466
+ // Stroke/border color priority: explicit stroke > span color (workflow context) > fill color
467
+ // spanColor provides border color in workflow context (from .spans.canvas)
468
468
  const nodeDataStroke = nodeData.stroke as string | undefined;
469
- const baseStrokeColor = nodeDataStroke || scopeColor || fillColor;
469
+ const baseStrokeColor = nodeDataStroke || spanColor || fillColor;
470
470
 
471
471
  // Apply status-based border styling
472
472
  const status = nodeData?.status as 'draft' | 'approved' | 'implemented' | undefined;
@@ -97,13 +97,15 @@ export const OtelBoundaryNode: React.FC<NodeProps<Node<OtelBoundaryNodeData>>> =
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#06b6d4';
103
+ // Fill color priority: explicit color > scope color > type definition color > default cyan
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#06b6d4';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -97,13 +97,15 @@ export const OtelEventNode: React.FC<NodeProps<Node<OtelEventNodeData>>> = ({
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#3b82f6';
103
+ // Fill color priority: explicit color > scope color > type definition color > default blue
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#3b82f6';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -97,13 +97,15 @@ export const OtelResourceNode: React.FC<NodeProps<Node<OtelResourceNodeData>>> =
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#f97316';
103
+ // Fill color priority: explicit color > scope color > type definition color > default orange
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#f97316';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -94,13 +94,15 @@ export const OtelScopeNode: React.FC<NodeProps<Node<OtelScopeNodeData>>> = ({
94
94
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
95
95
 
96
96
  // Color resolution
97
+ const scopeColor = nodeData.scopeColor as string | undefined;
97
98
  const spanColor = nodeData.spanColor as string | undefined;
98
99
  const nodeDataColor = nodeData.color as string | undefined;
99
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#22c55e';
100
+ // Fill color priority: explicit color > scope color > type definition color > default green
101
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#22c55e';
100
102
  const fillColor = baseFillColor;
101
- const scopeColor = nodeData.scopeColor as string | undefined;
103
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
102
104
  const nodeDataStroke = nodeData.stroke as string | undefined;
103
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
105
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
104
106
 
105
107
  // Display info
106
108
  const displayName = nodeProps.name;
@@ -113,14 +113,15 @@ export const OtelSpanConventionNode: React.FC<
113
113
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
114
114
 
115
115
  // Color resolution
116
+ const scopeColor = nodeData.scopeColor as string | undefined;
116
117
  const spanColor = nodeData.spanColor as string | undefined;
117
118
  const nodeDataColor = nodeData.color as string | undefined;
118
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#8b5cf6';
119
+ // Fill color priority: explicit color > scope color > type definition color > default purple
120
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#8b5cf6';
119
121
  const fillColor = baseFillColor;
120
-
121
- const scopeColor = nodeData.scopeColor as string | undefined;
122
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
122
123
  const nodeDataStroke = nodeData.stroke as string | undefined;
123
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
124
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
124
125
 
125
126
  // Get display info
126
127
  const displayName = nodeProps.name;
@@ -685,19 +685,17 @@ export const EdgeTypeDefinitions: Story = {
685
685
  story: `
686
686
  **Edge Type Definitions (PV Extension)**
687
687
 
688
- Define reusable edge types at the canvas level in \`pv.edgeTypes\`:
688
+ Define reusable edge types at the canvas level in \`edgeTypes\`:
689
689
 
690
690
  \`\`\`json
691
691
  {
692
- "pv": {
693
- "edgeTypes": {
694
- "depends-on": {
695
- "label": "Dependency",
696
- "style": "solid",
697
- "color": "#ef4444",
698
- "width": 2,
699
- "directed": true
700
- }
692
+ "edgeTypes": {
693
+ "depends-on": {
694
+ "label": "Dependency",
695
+ "style": "solid",
696
+ "color": "#ef4444",
697
+ "width": 2,
698
+ "directed": true
701
699
  }
702
700
  }
703
701
  }
@@ -917,23 +915,21 @@ const EdgeFieldsComparisonTemplate = () => {
917
915
  >
918
916
  <pre style={{ margin: 0 }}>
919
917
  {`{
920
- "pv": {
921
- "edgeTypes": {
922
- "depends-on": {
923
- "label": "Dependency", // Display name
924
- "style": "solid", // solid | dashed | dotted | animated
925
- "color": "#ef4444", // Hex color
926
- "width": 2, // Line width in pixels
927
- "directed": true, // Show arrow head
928
- "animation": { // Optional animation
929
- "type": "flow", // flow | pulse | particle | glow
930
- "duration": 1000, // Duration in ms
931
- "color": "#ff0000" // Animation color
932
- },
933
- "labelConfig": { // Label positioning
934
- "field": "weight", // Data field to display
935
- "position": "middle" // start | middle | end
936
- }
918
+ "edgeTypes": {
919
+ "depends-on": {
920
+ "label": "Dependency", // Display name
921
+ "style": "solid", // solid | dashed | dotted | animated
922
+ "color": "#ef4444", // Hex color
923
+ "width": 2, // Line width in pixels
924
+ "directed": true, // Show arrow head
925
+ "animation": { // Optional animation
926
+ "type": "flow", // flow | pulse | particle | glow
927
+ "duration": 1000, // Duration in ms
928
+ "color": "#ff0000" // Animation color
929
+ },
930
+ "labelConfig": { // Label positioning
931
+ "field": "weight", // Data field to display
932
+ "position": "middle" // start | middle | end
937
933
  }
938
934
  }
939
935
  }