@@ -8,7 +8,7 @@ import ReactFlow, {
88 useNodesState ,
99} from "reactflow" ;
1010
11- import { CenterFocusStrong , Info as InfoIcon , ZoomIn , ZoomOut } from "@mui/icons-material" ;
11+ import { CenterFocusStrong , Info as InfoIcon , Speed , Warning , ZoomIn , ZoomOut } from "@mui/icons-material" ;
1212import {
1313 Alert ,
1414 Box ,
@@ -36,6 +36,25 @@ import { StageNode, StageNodeName } from "./StageNode";
3636const options = { hideAttribution : true } ;
3737const nodeTypes = { [ StageNodeName ] : StageNode } ;
3838
39+ // Types for navigation data
40+ interface NodeWithAlert {
41+ nodeId : string ;
42+ position : { x : number ; y : number } ;
43+ alert : any ;
44+ }
45+
46+ interface BiggestDurationNode {
47+ nodeId : string ;
48+ position : { x : number ; y : number } ;
49+ durationPercentage : number ;
50+ }
51+
52+ interface NavigationData {
53+ nodesWithAlerts : NodeWithAlert [ ] ;
54+ biggestDurationNode : BiggestDurationNode | null ;
55+ nodesByDuration : BiggestDurationNode [ ] ;
56+ }
57+
3958const SqlFlow : FC < { sparkSQL : EnrichedSparkSQL } > = ( {
4059 sparkSQL,
4160} ) : JSX . Element => {
@@ -50,6 +69,8 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({
5069 const [ searchParams ] = useSearchParams ( ) ;
5170 const nodeIdsParam = searchParams . get ( 'nodeids' ) ;
5271 const initialFocusApplied = useRef < string | null > ( null ) ;
72+ const [ currentAlertIndex , setCurrentAlertIndex ] = useState ( 0 ) ;
73+ const [ currentDurationIndex , setCurrentDurationIndex ] = useState ( 0 ) ;
5374
5475 const dispatch = useAppDispatch ( ) ;
5576 const graphFilter = useAppSelector ( ( state ) => state . general . sqlMode ) ;
@@ -68,6 +89,33 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({
6889 return { totalNodes, totalEdges, highlightedNodes } ;
6990 } , [ nodes , edges , nodeIdsParam , sparkSQL ] ) ;
7091
92+ // Memoized calculations for navigation features
93+ const navigationData = useMemo ( ( ) : NavigationData => {
94+ if ( ! sparkSQL || ! nodes . length ) return { nodesWithAlerts : [ ] , biggestDurationNode : null , nodesByDuration : [ ] } ;
95+
96+ // Find nodes with alerts
97+ const nodesWithAlerts : NodeWithAlert [ ] = nodes . filter ( node => node . data ?. alert ) . map ( node => ( {
98+ nodeId : node . id ,
99+ position : node . position ,
100+ alert : node . data . alert
101+ } ) ) ;
102+
103+ // Get all nodes with duration percentage and sort by duration (highest first)
104+ const nodesByDuration : BiggestDurationNode [ ] = nodes
105+ . filter ( node => node . data ?. node ?. durationPercentage !== undefined )
106+ . map ( node => ( {
107+ nodeId : node . id ,
108+ position : node . position ,
109+ durationPercentage : node . data . node . durationPercentage !
110+ } ) )
111+ . sort ( ( a , b ) => b . durationPercentage - a . durationPercentage ) ;
112+
113+ // Find node with biggest duration percentage (first in sorted array)
114+ const biggestDurationNode : BiggestDurationNode | null = nodesByDuration . length > 0 ? nodesByDuration [ 0 ] : null ;
115+
116+ return { nodesWithAlerts, biggestDurationNode, nodesByDuration } ;
117+ } , [ nodes , sparkSQL ] ) ;
118+
71119 // Effect for metric updates only
72120 React . useEffect ( ( ) => {
73121 if ( ! sparkSQL ) return ;
@@ -160,7 +208,15 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({
160208 }
161209 } , [ instance , edges , nodeIdsParam ] ) ;
162210
163- useEffect ( ( ) => { } , [ nodes ] ) ;
211+ // Reset alert index when nodes change
212+ useEffect ( ( ) => {
213+ setCurrentAlertIndex ( 0 ) ;
214+ } , [ navigationData . nodesWithAlerts . length ] ) ;
215+
216+ // Reset duration index when nodes change
217+ useEffect ( ( ) => {
218+ setCurrentDurationIndex ( 0 ) ;
219+ } , [ navigationData . nodesByDuration . length ] ) ;
164220
165221 const onConnect = useCallback (
166222 ( params : any ) =>
@@ -192,6 +248,38 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({
192248 }
193249 } , [ instance ] ) ;
194250
251+ // Cycle through nodes by duration percentage (highest to lowest)
252+ const handleFocusNextDuration = useCallback ( ( ) => {
253+ if ( instance && navigationData . nodesByDuration . length > 0 ) {
254+ const nextIndex = ( currentDurationIndex + 1 ) % navigationData . nodesByDuration . length ;
255+ setCurrentDurationIndex ( nextIndex ) ;
256+
257+ const node = navigationData . nodesByDuration [ nextIndex ] ;
258+ const nodeWidth = 280 ;
259+ const nodeHeight = 280 ;
260+ const centerX = node . position . x + nodeWidth / 2 ;
261+ const centerY = node . position . y + nodeHeight / 2 ;
262+
263+ instance . setCenter ( centerX , centerY , { zoom : 0.75 } ) ;
264+ }
265+ } , [ instance , navigationData . nodesByDuration , currentDurationIndex ] ) ;
266+
267+ // Cycle through nodes with alerts
268+ const handleFocusNextAlert = useCallback ( ( ) => {
269+ if ( instance && navigationData . nodesWithAlerts . length > 0 ) {
270+ const nextIndex = ( currentAlertIndex + 1 ) % navigationData . nodesWithAlerts . length ;
271+ setCurrentAlertIndex ( nextIndex ) ;
272+
273+ const node = navigationData . nodesWithAlerts [ nextIndex ] ;
274+ const nodeWidth = 280 ;
275+ const nodeHeight = 280 ;
276+ const centerX = node . position . x + nodeWidth / 2 ;
277+ const centerY = node . position . y + nodeHeight / 2 ;
278+
279+ instance . setCenter ( centerX , centerY , { zoom : 0.75 } ) ;
280+ }
281+ } , [ instance , navigationData . nodesWithAlerts , currentAlertIndex ] ) ;
282+
195283 if ( error ) {
196284 return (
197285 < Box
@@ -287,6 +375,60 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({
287375 < CenterFocusStrong />
288376 </ IconButton >
289377 </ Tooltip >
378+
379+ < Tooltip
380+ title = { navigationData . nodesByDuration . length > 0
381+ ? `Focus on biggest node duration (${ currentDurationIndex + 1 } /${ navigationData . nodesByDuration . length } ) - ${ navigationData . nodesByDuration [ currentDurationIndex ] ?. durationPercentage . toFixed ( 1 ) } %`
382+ : "No duration data available"
383+ }
384+ arrow
385+ placement = "left"
386+ >
387+ < IconButton
388+ onClick = { handleFocusNextDuration }
389+ disabled = { navigationData . nodesByDuration . length === 0 }
390+ sx = { {
391+ backgroundColor : "rgba(245, 247, 250, 0.95)" ,
392+ color : navigationData . nodesByDuration . length > 0 ? "#424242" : "#bdbdbd" ,
393+ border : "1px solid rgba(0, 0, 0, 0.15)" ,
394+ "&:hover" : {
395+ backgroundColor : navigationData . nodesByDuration . length > 0 ? "rgba(245, 247, 250, 1)" : "rgba(245, 247, 250, 0.95)"
396+ } ,
397+ "&:disabled" : {
398+ backgroundColor : "rgba(245, 247, 250, 0.5)" ,
399+ }
400+ } }
401+ >
402+ < Speed />
403+ </ IconButton >
404+ </ Tooltip >
405+
406+ < Tooltip
407+ title = { navigationData . nodesWithAlerts . length > 0
408+ ? `Focus on alerts (${ currentAlertIndex + 1 } /${ navigationData . nodesWithAlerts . length } )`
409+ : "No alerts found"
410+ }
411+ arrow
412+ placement = "left"
413+ >
414+ < IconButton
415+ onClick = { handleFocusNextAlert }
416+ disabled = { navigationData . nodesWithAlerts . length === 0 }
417+ sx = { {
418+ backgroundColor : "rgba(245, 247, 250, 0.95)" ,
419+ color : navigationData . nodesWithAlerts . length > 0 ? "#424242" : "#bdbdbd" ,
420+ border : "1px solid rgba(0, 0, 0, 0.15)" ,
421+ "&:hover" : {
422+ backgroundColor : navigationData . nodesWithAlerts . length > 0 ? "rgba(245, 247, 250, 1)" : "rgba(245, 247, 250, 0.95)"
423+ } ,
424+ "&:disabled" : {
425+ backgroundColor : "rgba(245, 247, 250, 0.5)" ,
426+ }
427+ } }
428+ >
429+ < Warning />
430+ </ IconButton >
431+ </ Tooltip >
290432 </ Box >
291433
292434 < ReactFlow
0 commit comments