@@ -15,7 +15,13 @@ import { AlertTriangle, Loader2, ShieldAlert } from "lucide-react";
1515import { AttackPathCard } from "@/components/attack-path-card" ;
1616import { GraphLegend , FullscreenButton } from "@/components/graph-chrome" ;
1717import { LineageDetailPanel } from "@/components/lineage-detail" ;
18- import { FilterPanel , DEFAULT_FILTERS , type FilterState } from "@/components/lineage-filter" ;
18+ import {
19+ FilterPanel ,
20+ DEFAULT_FILTERS ,
21+ createExpandedGraphFilters ,
22+ createFocusedGraphFilters ,
23+ type FilterState ,
24+ } from "@/components/lineage-filter" ;
1925import { lineageNodeTypes , type LineageNodeData , type LineageNodeType } from "@/components/lineage-nodes" ;
2026import { applyDagreLayout } from "@/lib/dagre-layout" ;
2127import {
@@ -28,7 +34,9 @@ import {
2834 BACKGROUND_COLOR ,
2935 BACKGROUND_GAP ,
3036 CONTROLS_CLASS ,
37+ MINIMAP_BG ,
3138 MINIMAP_CLASS ,
39+ MINIMAP_MASK ,
3240 minimapNodeColor ,
3341} from "@/lib/graph-utils" ;
3442import {
@@ -328,6 +336,7 @@ export default function GraphPage() {
328336 const [ searching , setSearching ] = useState ( false ) ;
329337 const [ hoveredNodeId , setHoveredNodeId ] = useState < string | null > ( null ) ;
330338 const [ filters , setFilters ] = useState < FilterState > ( DEFAULT_FILTERS ) ;
339+ const [ initializedFocus , setInitializedFocus ] = useState ( false ) ;
331340
332341 useEffect ( ( ) => {
333342 setLoadingSnapshots ( true ) ;
@@ -372,6 +381,8 @@ export default function GraphPage() {
372381 setSearchResults ( [ ] ) ;
373382 setSearchQuery ( "" ) ;
374383 setSelectedAttackPathKey ( null ) ;
384+ setFilters ( DEFAULT_FILTERS ) ;
385+ setInitializedFocus ( false ) ;
375386 } , [ selectedScanId ] ) ;
376387
377388 useEffect ( ( ) => {
@@ -485,6 +496,12 @@ export default function GraphPage() {
485496 return buildUnifiedFlowGraph ( graphData , filters ) ;
486497 } , [ graphData , filters ] ) ;
487498
499+ useEffect ( ( ) => {
500+ if ( initializedFocus || flow . agentNames . length === 0 ) return ;
501+ setFilters ( createFocusedGraphFilters ( flow . agentNames [ 0 ] ) ) ;
502+ setInitializedFocus ( true ) ;
503+ } , [ flow . agentNames , initializedFocus ] ) ;
504+
488505 const flowNodeDataById = useMemo (
489506 ( ) => new Map ( flow . nodes . map ( ( node ) => [ node . id , node . data ] ) ) ,
490507 [ flow . nodes ] ,
@@ -494,14 +511,14 @@ export default function GraphPage() {
494511 ( ) =>
495512 flow . nodes . length > 0
496513 ? applyDagreLayout ( flow . nodes , flow . edges , {
497- direction : "LR" ,
498- nodeWidth : 180 ,
499- nodeHeight : 64 ,
500- rankSep : 110 ,
501- nodeSep : 28 ,
514+ direction : filters . agentName || filters . vulnOnly ? "TB" : "LR" ,
515+ nodeWidth : filters . agentName ? 208 : 188 ,
516+ nodeHeight : 72 ,
517+ rankSep : filters . agentName ? 118 : 104 ,
518+ nodeSep : filters . agentName ? 22 : 28 ,
502519 } )
503520 : { nodes : [ ] , edges : [ ] } ,
504- [ flow . nodes , flow . edges ] ,
521+ [ flow . nodes , flow . edges , filters . agentName , filters . vulnOnly ] ,
505522 ) ;
506523
507524 const connectedIds = useMemo (
@@ -680,7 +697,7 @@ export default function GraphPage() {
680697 < p className = "text-[10px] uppercase tracking-[0.24em] text-sky-400" > Unified graph</ p >
681698 < h1 className = "mt-1 text-lg font-semibold text-zinc-100" > Security Graph</ h1 >
682699 < p className = "text-xs text-zinc-500" >
683- Provider → Agent → Server → Package → Findings · Models · Datasets · Containers · Cloud resources · Runtime edges
700+ Focused agent-to-finding graph with packages, credentials, tools, and runtime links.
684701 </ p >
685702 </ div >
686703
@@ -735,6 +752,37 @@ export default function GraphPage() {
735752 </ button >
736753 </ form >
737754
755+ < div className = "mt-3 flex flex-wrap items-center gap-2 text-[11px]" >
756+ < button
757+ type = "button"
758+ onClick = { ( ) => setFilters ( createFocusedGraphFilters ( filters . agentName ?? flow . agentNames [ 0 ] ?? null ) ) }
759+ className = "rounded-lg border border-sky-500/30 bg-sky-500/10 px-2.5 py-1 text-sky-200 transition hover:border-sky-400/60"
760+ >
761+ Focused view
762+ </ button >
763+ < button
764+ type = "button"
765+ onClick = { ( ) => setFilters ( createExpandedGraphFilters ( null ) ) }
766+ className = "rounded-lg border border-zinc-700 bg-zinc-900/80 px-2.5 py-1 text-zinc-300 transition hover:border-zinc-500 hover:text-zinc-100"
767+ >
768+ Expanded view
769+ </ button >
770+ < span className = "rounded-lg border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 text-zinc-400" >
771+ { filters . agentName ? `agent ${ filters . agentName } ` : "all agents" }
772+ </ span >
773+ < span className = "rounded-lg border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 text-zinc-400" >
774+ { filters . severity ? `${ filters . severity } +` : "all severities" }
775+ </ span >
776+ < span className = "rounded-lg border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 text-zinc-400" >
777+ depth { filters . maxDepth }
778+ </ span >
779+ { filters . vulnOnly && (
780+ < span className = "rounded-lg border border-emerald-500/20 bg-emerald-500/10 px-2.5 py-1 text-emerald-200" >
781+ vulnerable only
782+ </ span >
783+ ) }
784+ </ div >
785+
738786 { searchResults . length > 0 && (
739787 < div className = "mt-2 rounded-2xl border border-zinc-800 bg-zinc-950/90 p-2" >
740788 < div className = "grid gap-2 md:grid-cols-2 xl:grid-cols-4" >
@@ -751,7 +799,12 @@ export default function GraphPage() {
751799 </ p >
752800 < div className = "mt-2 flex flex-wrap gap-2 text-[10px] text-zinc-500" >
753801 { result . severity && < span > { result . severity } </ span > }
754- < span > risk { result . risk_score . toFixed ( 1 ) } </ span >
802+ < span >
803+ risk{ " " }
804+ { typeof result . risk_score === "number" && Number . isFinite ( result . risk_score )
805+ ? result . risk_score . toFixed ( 1 )
806+ : "N/A" }
807+ </ span >
755808 </ div >
756809 </ button >
757810 ) ) }
@@ -893,8 +946,10 @@ export default function GraphPage() {
893946 edges = { displayEdges }
894947 nodeTypes = { lineageNodeTypes }
895948 fitView
896- minZoom = { 0.05 }
949+ fitViewOptions = { { padding : 0.18 , maxZoom : 1.05 } }
950+ minZoom = { 0.16 }
897951 maxZoom = { 2.5 }
952+ onlyRenderVisibleElements
898953 defaultEdgeOptions = { { type : "smoothstep" } }
899954 proOptions = { { hideAttribution : true } }
900955 onNodeClick = { onNodeClick }
@@ -908,7 +963,12 @@ export default function GraphPage() {
908963 >
909964 < Background color = { BACKGROUND_COLOR } gap = { BACKGROUND_GAP } />
910965 < Controls className = { CONTROLS_CLASS } />
911- < MiniMap nodeColor = { minimapNodeColor } className = { MINIMAP_CLASS } />
966+ < MiniMap
967+ nodeColor = { minimapNodeColor }
968+ className = { MINIMAP_CLASS }
969+ bgColor = { MINIMAP_BG }
970+ maskColor = { MINIMAP_MASK }
971+ />
912972 </ ReactFlow >
913973
914974 { selectedNode && (
0 commit comments