@@ -12,7 +12,13 @@ <h1>Graph Viewer</h1>
1212 < p class ="status "> Loading graph from the /extract endpoint...</ p >
1313 </ header >
1414 < main >
15- < div id ="graph "> </ div >
15+ < div class ="graph-container ">
16+ < div id ="graph "> </ div >
17+ < div class ="graph-filter ">
18+ < label for ="graph-filter-input "> Filter</ label >
19+ < input id ="graph-filter-input " type ="search " placeholder ="Filter nodes by label... " aria-label ="Filter graph nodes " />
20+ </ div >
21+ </ div >
1622 < aside class ="sidebar ">
1723 < h2 id ="sidebar-title "> No node selected</ h2 >
1824 < div id ="node-details "> < p > Select a node to view details.</ p > </ div >
@@ -24,6 +30,7 @@ <h2 id="sidebar-title">No node selected</h2>
2430 const status = document . querySelector ( '.status' ) ;
2531 const sidebarTitle = document . getElementById ( 'sidebar-title' ) ;
2632 const details = document . getElementById ( 'node-details' ) ;
33+ const filterInput = document . getElementById ( 'graph-filter-input' ) ;
2734
2835 const BASE_NODE_SIZE = 10 ;
2936
@@ -43,6 +50,10 @@ <h2 id="sidebar-title">No node selected</h2>
4350 }
4451
4552 function ensureNodeInView ( node ) {
53+ if ( node . hidden ( ) ) {
54+ return ;
55+ }
56+
4657 if ( ! isNodeInViewport ( node ) ) {
4758 node . cy ( ) . animate ( {
4859 center : { eles : node } ,
@@ -51,6 +62,15 @@ <h2 id="sidebar-title">No node selected</h2>
5162 }
5263 }
5364
65+ function fitViewToNodes ( nodes ) {
66+ if ( nodes . length > 0 ) {
67+ nodes . cy ( ) . animate ( {
68+ fit : { eles : nodes } ,
69+ duration : 500 ,
70+ } ) ;
71+ }
72+ }
73+
5474 function renderEntityDetails ( node ) {
5575 const data = node . data ( ) ;
5676
@@ -71,17 +91,21 @@ <h2 id="sidebar-title">No node selected</h2>
7191 } ) . join ( '' ) ;
7292 details . innerHTML = `<ul>${ aliasList } </ul>` ;
7393
94+ function switchSelectedNode ( nodeIdToSelect ) {
95+ const cy = node . cy ( ) ;
96+ const nodeToSelect = cy . getElementById ( nodeIdToSelect ) ;
97+ if ( nodeToSelect . length > 0 ) {
98+ cy . elements ( ) . unselect ( ) ;
99+ nodeToSelect . select ( ) ;
100+ ensureNodeInView ( nodeToSelect ) ;
101+ }
102+ }
103+
74104 document . querySelectorAll ( '#node-details li a[data-alias-id]' ) . forEach ( item => {
75105 item . addEventListener ( 'click' , ( e ) => {
76106 const aliasId = e . target . getAttribute ( 'data-alias-id' ) ;
77107 if ( aliasId ) {
78- const cy = node . cy ( ) ;
79- const aliasNode = cy . getElementById ( aliasId ) ;
80- if ( aliasNode . length > 0 ) {
81- cy . elements ( ) . unselect ( ) ;
82- aliasNode . select ( ) ;
83- ensureNodeInView ( aliasNode ) ;
84- }
108+ switchSelectedNode ( aliasId ) ;
85109 }
86110 } ) ;
87111 } ) ;
@@ -134,6 +158,32 @@ <h2 id="sidebar-title">No node selected</h2>
134158 return BASE_NODE_SIZE + ( occurrences . length * 2 ) ;
135159 }
136160
161+ function resetSidebar ( ) {
162+ sidebarTitle . textContent = 'No node selected' ;
163+ details . innerHTML = '<p>Select a node to view details.</p>' ;
164+ }
165+
166+ function filterGraph ( cy , filterTerm ) {
167+ cy . nodes ( ) . forEach ( ( node ) => {
168+ const label = node . data ( 'label' ) ;
169+ const shouldShowNode =
170+ ! filterTerm || label . toLowerCase ( ) . includes ( filterTerm ) ;
171+
172+ if ( shouldShowNode ) {
173+ node . show ( ) ;
174+ } else {
175+ node . hide ( ) ;
176+ }
177+ } ) ;
178+
179+ const selectedNodes = cy . $ ( 'node:selected' ) ;
180+ if ( selectedNodes . every ( ( node ) => node . hidden ( ) ) ) {
181+ selectedNodes . unselect ( ) ;
182+ resetSidebar ( ) ;
183+ }
184+ fitViewToNodes ( cy . nodes ( ':visible' ) ) ;
185+ }
186+
137187 async function loadGraph ( ) {
138188 try {
139189 const response = await fetch ( '/graph-viewmodel?run_path={{ run_path }}' ) ;
@@ -209,7 +259,7 @@ <h2 id="sidebar-title">No node selected</h2>
209259 selector : ':selected' ,
210260 style : {
211261 'border-width' : 1.5 ,
212- 'border-color' : '#ffdd00'
262+ 'border-color' : '#ffdd00' ,
213263 }
214264 }
215265 ] ,
@@ -223,14 +273,21 @@ <h2 id="sidebar-title">No node selected</h2>
223273 }
224274 } ) ;
225275
276+ let filterUpdateTimeout ;
277+ filterInput . addEventListener ( 'input' , ( event ) => {
278+ const filterTerm = event . target . value . trim ( ) . toLowerCase ( ) ;
279+
280+ clearTimeout ( filterUpdateTimeout ) ;
281+ filterUpdateTimeout = setTimeout ( ( ) => { filterGraph ( cy , filterTerm ) ; } , 500 ) ;
282+ } ) ;
283+
226284 cy . on ( 'select' , 'node' , ( event ) => {
227285 renderNodeDetails ( event . target ) ;
228286 } ) ;
229287
230288 cy . on ( 'tap' , ( event ) => {
231289 if ( event . target === cy ) {
232- sidebarTitle . textContent = 'No node selected' ;
233- details . innerHTML = '<p>Select a node to view details.</p>' ;
290+ resetSidebar ( ) ;
234291 }
235292 } ) ;
236293 } catch ( error ) {
0 commit comments