@@ -15,12 +15,14 @@ import {
1515} from '@xyflow/react'
1616import '@xyflow/react/dist/style.css'
1717import { useNavigate } from 'react-router-dom'
18+ import { X } from 'lucide-react'
1819import {
1920 Tooltip ,
2021 TooltipContent ,
2122 TooltipProvider ,
2223 TooltipTrigger ,
2324} from '@/components/ui/tooltip'
25+ import { Button } from '@/components/ui/button'
2426import {
2527 layoutWithELK ,
2628 NODE_WIDTH ,
@@ -35,6 +37,8 @@ import {
3537 type ManifestGraph as ManifestGraphModel ,
3638} from '../lib/manifest-graph-model'
3739import type { Manifest } from '@/api/gen/meridian/control_plane/v1/manifest_pb'
40+ import { useEventChain } from '../hooks/use-event-chain'
41+ import { EventChainPanel } from './event-chain-panel'
3842
3943// Theme colors per node type
4044const NODE_THEMES : Record < ManifestNodeType , { color : string ; label : string } > = {
@@ -316,12 +320,30 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
316320 const [ nodes , setNodes , onNodesChange ] = useNodesState < Node > ( [ ] )
317321 const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge > ( [ ] )
318322 const [ hoveredNode , setHoveredNode ] = useState < string | null > ( null )
323+ const [ selectedNode , setSelectedNode ] = useState < string | null > ( null )
324+ const [ showEventChain , setShowEventChain ] = useState ( false )
319325 const [ visibleTypes , setVisibleTypes ] = useState < Set < ManifestNodeType > > (
320326 ( ) => new Set < ManifestNodeType > ( [ 'instrument' , 'account_type' , 'valuation_rule' , 'saga' ] ) ,
321327 )
322328
323329 const graph = useMemo ( ( ) => buildManifestGraph ( manifest ) , [ manifest ] )
324330
331+ // Derive effective selection: clear if the selected node no longer exists in the graph
332+ const effectiveSelectedNode = useMemo (
333+ ( ) => ( selectedNode && graph . nodes . some ( ( n ) => n . id === selectedNode ) ? selectedNode : null ) ,
334+ [ selectedNode , graph ] ,
335+ )
336+
337+ const selectedManifestNode = useMemo (
338+ ( ) => ( effectiveSelectedNode ? graph . nodes . find ( ( n ) => n . id === effectiveSelectedNode ) ?? null : null ) ,
339+ [ graph , effectiveSelectedNode ] ,
340+ )
341+
342+ const canShowEventChain = selectedManifestNode ?. type === 'instrument' || selectedManifestNode ?. type === 'account_type'
343+
344+ const eventChainNodeId = showEventChain ? effectiveSelectedNode : null
345+ const eventChain = useEventChain ( graph , eventChainNodeId )
346+
325347 const nodeCountByType = useMemo ( ( ) => {
326348 const counts : Record < ManifestNodeType , number > = {
327349 instrument : 0 ,
@@ -363,13 +385,14 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
363385 return ( ) => { cancelled = true }
364386 } , [ graph , visibleTypes , setNodes , setEdges ] )
365387
366- // Hover highlighting
388+ // Hover + selection highlighting
367389 useEffect ( ( ) => {
390+ const activeNode = hoveredNode ?? effectiveSelectedNode
368391 const connectedNodes = new Set < string > ( )
369- if ( hoveredNode ) {
370- connectedNodes . add ( hoveredNode )
392+ if ( activeNode ) {
393+ connectedNodes . add ( activeNode )
371394 for ( const e of currentEdges ) {
372- if ( e . source === hoveredNode || e . target === hoveredNode ) {
395+ if ( e . source === activeNode || e . target === activeNode ) {
373396 connectedNodes . add ( e . source )
374397 connectedNodes . add ( e . target )
375398 }
@@ -379,8 +402,8 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
379402 setNodes ( ( nds ) => {
380403 let changed = false
381404 const next = nds . map ( ( n ) => {
382- const highlighted = hoveredNode ? n . id === hoveredNode : false
383- const dimmed = hoveredNode ? ! connectedNodes . has ( n . id ) : false
405+ const highlighted = activeNode ? n . id === activeNode : false
406+ const dimmed = activeNode ? ! connectedNodes . has ( n . id ) : false
384407 const current = n . data as ManifestNodeData
385408 if ( current . highlighted === highlighted && current . dimmed === dimmed ) return n
386409 changed = true
@@ -399,9 +422,17 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
399422 } )
400423 return changed ? next : eds
401424 } )
402- } , [ hoveredNode , currentEdges , setNodes , setEdges ] )
425+ } , [ hoveredNode , effectiveSelectedNode , currentEdges , setNodes , setEdges ] )
403426
404427 const onNodeClick : NodeMouseHandler = useCallback (
428+ ( _event , node ) => {
429+ setSelectedNode ( ( prev ) => ( prev === node . id ? null : node . id ) )
430+ setShowEventChain ( false )
431+ } ,
432+ [ ] ,
433+ )
434+
435+ const onNodeDoubleClick : NodeMouseHandler = useCallback (
405436 ( _event , node ) => {
406437 const data = node . data as ManifestNodeData
407438 const mn = data . manifestNode
@@ -420,6 +451,11 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
420451 [ navigate ] ,
421452 )
422453
454+ const onPaneClick = useCallback ( ( ) => {
455+ setSelectedNode ( null )
456+ setShowEventChain ( false )
457+ } , [ ] )
458+
423459 const onNodeMouseEnter : NodeMouseHandler = useCallback ( ( _event , node ) => {
424460 setHoveredNode ( node . id )
425461 } , [ ] )
@@ -438,7 +474,12 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
438474 }
439475 return next
440476 } )
441- } , [ ] )
477+ // Clear selection if the selected node's type was just hidden
478+ if ( selectedManifestNode ?. type === type ) {
479+ setSelectedNode ( null )
480+ setShowEventChain ( false )
481+ }
482+ } , [ selectedManifestNode ] )
442483
443484 const totalVisible = nodes . length
444485
@@ -461,6 +502,8 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
461502 onNodesChange = { onNodesChange }
462503 onEdgesChange = { onEdgesChange }
463504 onNodeClick = { onNodeClick }
505+ onNodeDoubleClick = { onNodeDoubleClick }
506+ onPaneClick = { onPaneClick }
464507 onNodeMouseEnter = { onNodeMouseEnter }
465508 onNodeMouseLeave = { onNodeMouseLeave }
466509 nodeTypes = { nodeTypes }
@@ -510,6 +553,67 @@ export function ManifestGraph({ manifest, className }: ManifestGraphProps) {
510553 < LegendItem key = { item . label } { ...item } />
511554 ) ) }
512555 </ div >
556+
557+ { /* Selection toolbar */ }
558+ { selectedManifestNode && (
559+ < div
560+ className = "absolute top-3 right-3 z-10 flex items-center gap-2 rounded-lg border bg-background/95 p-2 backdrop-blur-sm shadow-sm"
561+ data-testid = "node-toolbar"
562+ >
563+ < span className = "text-xs font-medium text-foreground px-1" >
564+ { selectedManifestNode . label }
565+ </ span >
566+ { canShowEventChain && (
567+ < Button
568+ size = "sm"
569+ variant = "outline"
570+ className = "text-xs h-7"
571+ onClick = { ( ) => setShowEventChain ( true ) }
572+ data-testid = "show-event-chain-button"
573+ >
574+ Show Event Chain
575+ </ Button >
576+ ) }
577+ < Button
578+ size = "sm"
579+ variant = "ghost"
580+ className = "h-7 w-7 p-0"
581+ onClick = { ( ) => { setSelectedNode ( null ) ; setShowEventChain ( false ) } }
582+ aria-label = "Deselect node"
583+ >
584+ < X className = "h-3.5 w-3.5" />
585+ </ Button >
586+ </ div >
587+ ) }
588+
589+ { /* Event chain side panel */ }
590+ { showEventChain && eventChain && selectedManifestNode && (
591+ < div
592+ className = "absolute top-0 right-0 z-20 h-full w-96 border-l bg-background shadow-lg overflow-y-auto"
593+ data-testid = "event-chain-side-panel"
594+ >
595+ < div className = "flex items-center justify-between p-3 border-b" >
596+ < h3 className = "text-sm font-semibold" > Event Chain</ h3 >
597+ < Button
598+ size = "sm"
599+ variant = "ghost"
600+ className = "h-7 w-7 p-0"
601+ onClick = { ( ) => { setSelectedNode ( null ) ; setShowEventChain ( false ) } }
602+ aria-label = "Close event chain panel"
603+ data-testid = "close-event-chain-panel"
604+ >
605+ < X className = "h-3.5 w-3.5" />
606+ </ Button >
607+ </ div >
608+ < div className = "p-3" >
609+ < EventChainPanel
610+ chain = { eventChain }
611+ startNodeLabel = { selectedManifestNode . label }
612+ onSagaClick = { ( sagaId ) => navigate ( `/sagas/${ sagaId . replace ( / ^ s a g a : / , '' ) } ` ) }
613+ />
614+ </ div >
615+ </ div >
616+ ) }
513617 </ div >
514618 )
515619}
0 commit comments