@principal-ai/principal-view-react 0.13.20 → 0.13.22

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.
@@ -1,11 +1,12 @@
1
- import React, { useState, useRef } from 'react';
2
- import { Handle, Position, NodeResizer } from '@xyflow/react';
1
+ import React, { useState, useRef, useCallback } from 'react';
2
+ import { Handle, Position, NodeResizer, useNodeId } from '@xyflow/react';
3
3
  import type { NodeProps, Node } from '@xyflow/react';
4
4
  import type { NodeTypeDefinition } from '@principal-ai/principal-view-core';
5
5
  import { useTheme } from '@principal-ade/industry-theme';
6
6
  import { resolveIcon } from '../utils/iconResolver';
7
7
  import { NodeTooltip } from '../components/NodeTooltip';
8
8
  import type { OtelInfo } from '../components/NodeTooltip';
9
+ import { useGraphEdit } from '../contexts/GraphEditContext';
9
10
 
10
11
  /**
11
12
  * Converts a hex color to a lighter/tinted version (opaque, not transparent)
@@ -63,6 +64,8 @@ export interface CustomNodeData extends Record<string, unknown> {
63
64
  */
64
65
  export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, selected, dragging }) => {
65
66
  const { theme } = useTheme();
67
+ const { onNodeResizeEnd } = useGraphEdit();
68
+ const nodeId = useNodeId();
66
69
  const [isHovered, setIsHovered] = useState(false);
67
70
  const nodeRef = useRef<HTMLDivElement>(null);
68
71
  const nodeProps = data;
@@ -86,10 +89,26 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
86
89
  // Inactive nodes (scenario filtering) are dimmed to 0.1
87
90
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
88
91
 
92
+ // Handle resize end - notify parent to track the dimension change
93
+ const handleResizeEnd = useCallback(
94
+ (_event: unknown, params: { width: number; height: number }) => {
95
+ if (nodeId && onNodeResizeEnd && params.width && params.height) {
96
+ onNodeResizeEnd(nodeId, {
97
+ width: Math.round(params.width),
98
+ height: Math.round(params.height),
99
+ });
100
+ }
101
+ },
102
+ [nodeId, onNodeResizeEnd]
103
+ );
104
+
89
105
  // DEBUG: Log ALL node data to understand structure
90
106
  console.log('[CustomNode] Node data:', {
91
107
  name: nodeProps.name,
92
108
  nodeDataKeys: nodeData ? Object.keys(nodeData).join(', ') : 'undefined',
109
+ references: nodeData?.references,
110
+ otelFiles: (nodeData?.otel as { files?: string[] })?.files,
111
+ sources: nodeData?.sources,
93
112
  fullNodeData: nodeData,
94
113
  });
95
114
 
@@ -99,10 +118,13 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
99
118
  const showTooltip =
100
119
  (isHovered && !dragging && shiftKeyPressed) || (!editable && !!selected);
101
120
 
102
- // Extract OTEL info, description, and sources for tooltip
103
- const otelInfo = nodeData?.otel as OtelInfo | undefined;
121
+ // Extract OTEL info, description, sources/files, and references for tooltip
122
+ const otelInfo = nodeData?.otel as (OtelInfo & { files?: string[] }) | undefined;
104
123
  const description = nodeData?.description as string | undefined;
105
- const sources = nodeData?.sources as string[] | undefined;
124
+ const sources = nodeData?.sources as string[] | undefined; // deprecated
125
+ const references = nodeData?.references as string[] | undefined;
126
+ // Files from otel.files - these are source code files where the event is instrumented
127
+ const files = otelInfo?.files;
106
128
 
107
129
  // Get badge shape styles based on node shape
108
130
  const getBadgeShapeStyles = (): React.CSSProperties => {
@@ -141,24 +163,28 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
141
163
  };
142
164
 
143
165
  // Get badge position based on shape - diamonds need badges at their points, not bounding box corners
144
- const getBadgePosition = (position: 'top-left' | 'top-right' | 'left' | 'right'): React.CSSProperties => {
166
+ const getBadgePosition = (position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | 'top' | 'bottom'): React.CSSProperties => {
145
167
  const isDiamondShape = typeDefinition.shape === 'diamond';
146
168
 
147
169
  if (isDiamondShape) {
148
170
  // Diamond points are at the middle of each edge of the bounding box
149
171
  switch (position) {
150
172
  case 'top-left':
151
- // Position at the LEFT point of the diamond (center-left)
152
- return { top: '50%', left: 0, transform: 'translate(-50%, -50%)' };
153
- case 'top-right':
154
- // Position at the RIGHT point of the diamond (center-right)
155
- return { top: '50%', right: 0, transform: 'translate(50%, -50%)' };
156
173
  case 'left':
157
174
  // Position at the LEFT point of the diamond (center-left)
158
175
  return { top: '50%', left: 0, transform: 'translate(-50%, -50%)' };
176
+ case 'top-right':
159
177
  case 'right':
160
178
  // Position at the RIGHT point of the diamond (center-right)
161
179
  return { top: '50%', right: 0, transform: 'translate(50%, -50%)' };
180
+ case 'top':
181
+ // Position at the TOP point of the diamond
182
+ return { top: 0, left: '50%', transform: 'translate(-50%, -50%)' };
183
+ case 'bottom':
184
+ case 'bottom-left':
185
+ case 'bottom-right':
186
+ // Position at the BOTTOM point of the diamond
187
+ return { bottom: 0, left: '50%', transform: 'translate(-50%, 50%)' };
162
188
  }
163
189
  }
164
190
 
@@ -168,17 +194,27 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
168
194
  return { top: -6, left: -6 };
169
195
  case 'top-right':
170
196
  return { top: -6, right: -6 };
197
+ case 'bottom-left':
198
+ return { bottom: -6, left: -6 };
199
+ case 'bottom-right':
200
+ return { bottom: -6, right: -6 };
171
201
  case 'left':
172
202
  return { top: -6, left: -6 };
173
203
  case 'right':
174
204
  return { top: -6, right: -6 };
205
+ case 'top':
206
+ return { top: -6, left: '50%', transform: 'translateX(-50%)' };
207
+ case 'bottom':
208
+ return { bottom: -6, left: '50%', transform: 'translateX(-50%)' };
175
209
  }
176
210
  };
177
211
 
178
- // Render Sources badge (top-right, or right point for diamonds)
212
+ // Render Sources badge (top-right) - shows "S" for files where event is instrumented
179
213
  const renderSourcesBadge = () => {
180
- const sources = nodeData?.sources as string[] | undefined;
181
- if (!sources || sources.length === 0) return null;
214
+ // Use otel.files (source code files where event is instrumented)
215
+ // Fall back to deprecated sources field for backwards compatibility
216
+ const sourceFiles = files || sources;
217
+ if (!sourceFiles || sourceFiles.length === 0) return null;
182
218
 
183
219
  const shapeStyles = getBadgeShapeStyles();
184
220
  const positionStyles = getBadgePosition('top-right');
@@ -203,13 +239,47 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
203
239
  zIndex: 10,
204
240
  opacity: nodeOpacity,
205
241
  }}
206
- title={`Sources: ${sources.join(', ')}`}
242
+ title={`Source files: ${sourceFiles.join(', ')}`}
207
243
  >
208
244
  <span style={{ transform: shapeStyles.transform ? 'rotate(-45deg)' : undefined }}>S</span>
209
245
  </div>
210
246
  );
211
247
  };
212
248
 
249
+ // Render References badge (bottom-left for rectangles, bottom for diamonds) - shows "R" for external references
250
+ const renderReferencesBadge = () => {
251
+ if (!references || references.length === 0) return null;
252
+
253
+ const shapeStyles = getBadgeShapeStyles();
254
+ const positionStyles = getBadgePosition('bottom-left');
255
+
256
+ return (
257
+ <div
258
+ style={{
259
+ position: 'absolute',
260
+ ...positionStyles,
261
+ ...shapeStyles,
262
+ // Override transform if shape has rotation but we already have a position transform
263
+ ...(typeDefinition.shape === 'diamond' ? { transform: `${positionStyles.transform} rotate(45deg)` } : {}),
264
+ backgroundColor: '#8b5cf6', // Purple for references
265
+ color: 'white',
266
+ fontSize: theme.fontSizes[0],
267
+ fontWeight: theme.fontWeights.bold,
268
+ fontFamily: theme.fonts.body,
269
+ display: 'flex',
270
+ alignItems: 'center',
271
+ justifyContent: 'center',
272
+ boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
273
+ zIndex: 10,
274
+ opacity: nodeOpacity,
275
+ }}
276
+ title={`References: ${references.join(', ')}`}
277
+ >
278
+ <span style={{ transform: shapeStyles.transform ? 'rotate(-45deg)' : undefined }}>R</span>
279
+ </div>
280
+ );
281
+ };
282
+
213
283
  // Render Boundary badge (right/left points) - shown instead of sources badge for boundary nodes
214
284
  const renderBoundaryBadge = () => {
215
285
  const boundary = nodeData?.boundary as { direction?: 'outbound' | 'inbound'; node?: Record<string, string> } | undefined;
@@ -619,6 +689,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
619
689
  minWidth={minWidth}
620
690
  minHeight={minHeight}
621
691
  keepAspectRatio={keepAspectRatio}
692
+ onResizeEnd={handleResizeEnd}
622
693
  handleStyle={{
623
694
  width: 8,
624
695
  height: 8,
@@ -662,7 +733,12 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
662
733
  onMouseLeave={() => setIsHovered(false)}
663
734
  >
664
735
  {renderStatusBadge()}
665
- {isBoundaryNode ? renderBoundaryBadge() : renderSourcesBadge()}
736
+ {isBoundaryNode ? renderBoundaryBadge() : (
737
+ <>
738
+ {renderSourcesBadge()}
739
+ {renderReferencesBadge()}
740
+ </>
741
+ )}
666
742
  <div style={hexagonBorderStyle} className={animationClass}>
667
743
  <div style={hexagonInnerStyle}>
668
744
  {icon && (
@@ -705,6 +781,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
705
781
  description={description}
706
782
  otel={otelInfo}
707
783
  sources={sources}
784
+ references={references}
708
785
  visible={showTooltip}
709
786
  nodeRef={nodeRef}
710
787
  />
@@ -728,7 +805,12 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
728
805
  onMouseLeave={() => setIsHovered(false)}
729
806
  >
730
807
  {renderStatusBadge()}
731
- {isBoundaryNode ? renderBoundaryBadge() : renderSourcesBadge()}
808
+ {isBoundaryNode ? renderBoundaryBadge() : (
809
+ <>
810
+ {renderSourcesBadge()}
811
+ {renderReferencesBadge()}
812
+ </>
813
+ )}
732
814
  <div style={diamondBorderStyle} className={animationClass}>
733
815
  <div style={diamondInnerStyle}>
734
816
  {icon && (
@@ -771,6 +853,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
771
853
  description={description}
772
854
  otel={otelInfo}
773
855
  sources={sources}
856
+ references={references}
774
857
  visible={showTooltip}
775
858
  nodeRef={nodeRef}
776
859
  />
@@ -784,7 +867,12 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
784
867
  onMouseLeave={() => setIsHovered(false)}
785
868
  >
786
869
  {renderStatusBadge()}
787
- {isBoundaryNode ? renderBoundaryBadge() : renderSourcesBadge()}
870
+ {isBoundaryNode ? renderBoundaryBadge() : (
871
+ <>
872
+ {renderSourcesBadge()}
873
+ {renderReferencesBadge()}
874
+ </>
875
+ )}
788
876
  <div style={getShapeStyles()} className={animationClass}>
789
877
  {/* Inner content */}
790
878
  <div
@@ -852,6 +940,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = ({ data, se
852
940
  description={description}
853
941
  otel={otelInfo}
854
942
  sources={sources}
943
+ references={references}
855
944
  visible={showTooltip}
856
945
  nodeRef={nodeRef}
857
946
  />
@@ -443,3 +443,187 @@ The badges on diamond nodes are positioned at the actual points of the diamond s
443
443
  },
444
444
  },
445
445
  };
446
+
447
+ /**
448
+ * Canvas demonstrating the new files (S) and references (R) badges
449
+ */
450
+ const filesAndReferencesCanvas: ExtendedCanvas = {
451
+ nodes: [
452
+ // Node with otel.files only (S badge)
453
+ {
454
+ id: 'files-only',
455
+ type: 'text',
456
+ x: 100,
457
+ y: 100,
458
+ width: 140,
459
+ height: 80,
460
+ text: 'Files Only',
461
+ color: 4, // green
462
+ pv: {
463
+ nodeType: 'event',
464
+ shape: 'rectangle',
465
+ icon: 'FileCode',
466
+ otel: {
467
+ kind: 'type',
468
+ files: ['src/auth/login.ts', 'src/auth/logout.ts'],
469
+ },
470
+ },
471
+ },
472
+ // Node with references only (R badge)
473
+ {
474
+ id: 'refs-only',
475
+ type: 'text',
476
+ x: 300,
477
+ y: 100,
478
+ width: 140,
479
+ height: 80,
480
+ text: 'Refs Only',
481
+ color: 6, // purple
482
+ pv: {
483
+ nodeType: 'event',
484
+ shape: 'rectangle',
485
+ icon: 'ExternalLink',
486
+ references: ['@opentelemetry/api', 'https://opentelemetry.io/docs'],
487
+ },
488
+ },
489
+ // Node with both files and references (S + R badges)
490
+ {
491
+ id: 'both-badges',
492
+ type: 'text',
493
+ x: 500,
494
+ y: 100,
495
+ width: 140,
496
+ height: 80,
497
+ text: 'Both Badges',
498
+ color: 2, // orange
499
+ pv: {
500
+ nodeType: 'event',
501
+ shape: 'rectangle',
502
+ icon: 'Layers',
503
+ otel: {
504
+ kind: 'service',
505
+ files: ['src/api/handler.ts'],
506
+ },
507
+ references: ['@principal-ai/core'],
508
+ },
509
+ },
510
+ // Diamond with files
511
+ {
512
+ id: 'diamond-files',
513
+ type: 'text',
514
+ x: 100,
515
+ y: 250,
516
+ width: 100,
517
+ height: 100,
518
+ text: 'Files',
519
+ color: 4,
520
+ pv: {
521
+ nodeType: 'decision',
522
+ shape: 'diamond',
523
+ icon: 'FileCode',
524
+ otel: {
525
+ kind: 'type',
526
+ files: ['src/router.ts'],
527
+ },
528
+ },
529
+ },
530
+ // Diamond with references
531
+ {
532
+ id: 'diamond-refs',
533
+ type: 'text',
534
+ x: 280,
535
+ y: 250,
536
+ width: 100,
537
+ height: 100,
538
+ text: 'Refs',
539
+ color: 6,
540
+ pv: {
541
+ nodeType: 'decision',
542
+ shape: 'diamond',
543
+ icon: 'ExternalLink',
544
+ references: ['https://docs.example.com'],
545
+ },
546
+ },
547
+ // Diamond with both
548
+ {
549
+ id: 'diamond-both',
550
+ type: 'text',
551
+ x: 460,
552
+ y: 250,
553
+ width: 100,
554
+ height: 100,
555
+ text: 'Both',
556
+ color: 2,
557
+ pv: {
558
+ nodeType: 'decision',
559
+ shape: 'diamond',
560
+ icon: 'Layers',
561
+ otel: {
562
+ kind: 'instance',
563
+ files: ['src/decision.ts'],
564
+ },
565
+ references: ['@some/package'],
566
+ },
567
+ },
568
+ // Circle with both badges
569
+ {
570
+ id: 'circle-both',
571
+ type: 'text',
572
+ x: 640,
573
+ y: 250,
574
+ width: 100,
575
+ height: 100,
576
+ text: 'Circle',
577
+ color: 5,
578
+ pv: {
579
+ nodeType: 'event',
580
+ shape: 'circle',
581
+ icon: 'Circle',
582
+ otel: {
583
+ kind: 'service',
584
+ files: ['src/service.ts'],
585
+ },
586
+ references: ['https://api.docs.com'],
587
+ },
588
+ },
589
+ ],
590
+ edges: [],
591
+ pv: {
592
+ version: '1.0.0',
593
+ name: 'Files and References Badges',
594
+ description: 'Demonstrates otel.files (S) and references (R) badges',
595
+ edgeTypes: {},
596
+ },
597
+ };
598
+
599
+ export const FilesAndReferences: Story = {
600
+ args: {
601
+ canvas: filesAndReferencesCanvas,
602
+ width: 850,
603
+ height: 450,
604
+ },
605
+ parameters: {
606
+ docs: {
607
+ description: {
608
+ story: `
609
+ **Files (S) and References (R) Badges**
610
+
611
+ Two types of badges indicate different metadata:
612
+
613
+ - **S badge** (green): Shows source files where the event is instrumented (\`pv.otel.files\`)
614
+ - **R badge** (purple): Shows external references like packages or documentation (\`pv.references\`)
615
+
616
+ **Row 1 - Rectangles:**
617
+ - Files Only: Has \`otel.files\` → shows S badge
618
+ - Refs Only: Has \`references\` → shows R badge
619
+ - Both Badges: Has both → shows S and R badges side by side
620
+
621
+ **Row 2 - Other Shapes:**
622
+ - Diamond, Circle shapes with various badge combinations
623
+
624
+ Hover over nodes to see the full file paths and reference URLs in the tooltip.
625
+ `,
626
+ },
627
+ },
628
+ },
629
+ };
@@ -676,7 +676,6 @@ const otelLogAssociationCanvas: ExtendedCanvas = {
676
676
  otel: {
677
677
  kind: 'type',
678
678
  category: 'log',
679
- isNew: true,
680
679
  },
681
680
  shape: 'rectangle',
682
681
  icon: 'FileText',
@@ -737,7 +736,6 @@ const otelLogAssociationCanvas: ExtendedCanvas = {
737
736
  otel: {
738
737
  kind: 'service',
739
738
  category: 'router',
740
- isNew: true,
741
739
  },
742
740
  shape: 'hexagon',
743
741
  icon: 'GitBranch',
@@ -861,7 +859,6 @@ const otelLogAssociationCanvas: ExtendedCanvas = {
861
859
  otel: {
862
860
  kind: 'service',
863
861
  category: 'collector',
864
- isNew: true,
865
862
  },
866
863
  shape: 'hexagon',
867
864
  icon: 'BarChart2',
@@ -884,7 +881,6 @@ const otelLogAssociationCanvas: ExtendedCanvas = {
884
881
  otel: {
885
882
  kind: 'type',
886
883
  category: 'audit',
887
- isNew: true,
888
884
  },
889
885
  shape: 'rectangle',
890
886
  icon: 'AlertTriangle',
@@ -149,8 +149,7 @@ export const BasicOtelEventNode: Story = {
149
149
  icon: "Play",
150
150
  otel: {
151
151
  kind: "event",
152
- category: "lifecycle",
153
- isNew: true
152
+ category: "lifecycle"
154
153
  },
155
154
  event: {
156
155
  name: "analysis.started",
@@ -343,8 +342,7 @@ export const NodeWithStateAndViolations: Story = {
343
342
  icon: "XCircle",
344
343
  otel: {
345
344
  kind: "event",
346
- category: "error",
347
- isNew: false
345
+ category: "error"
348
346
  },
349
347
  event: {
350
348
  name: "validation.error",
@@ -37,7 +37,6 @@ interface OtelLog {
37
37
  otel: {
38
38
  kind: 'type',
39
39
  category: 'log',
40
- isNew: true,
41
40
  },
42
41
  shape: 'rectangle',
43
42
  icon: 'FileText',
@@ -69,7 +68,6 @@ Routes incoming OTEL logs to canvas nodes based on matching criteria.
69
68
  otel: {
70
69
  kind: 'service',
71
70
  category: 'router',
72
- isNew: true,
73
71
  },
74
72
  shape: 'hexagon',
75
73
  icon: 'GitBranch',
@@ -92,7 +90,6 @@ Routes incoming OTEL logs to canvas nodes based on matching criteria.
92
90
  otel: {
93
91
  kind: 'type',
94
92
  category: 'audit',
95
- isNew: true,
96
93
  },
97
94
  shape: 'rectangle',
98
95
  icon: 'AlertTriangle',
@@ -210,7 +207,7 @@ export const TooltipVariants: StoryObj = {
210
207
  </div>
211
208
  <NodeTooltip
212
209
  description="OpenTelemetry log record with timestamp, severity, body"
213
- otel={{ kind: 'type', category: 'log', isNew: true }}
210
+ otel={{ kind: 'type', category: 'log' }}
214
211
  visible={true}
215
212
  />
216
213
  </div>