@@ -37,6 +37,7 @@ import {
3737import {
3838 CONTEXTUAL_MAP_DEFAULT_MAX_VISIBLE_NODES ,
3939 createChainServiceMap ,
40+ createHighFanOutServiceMap ,
4041 createRichContextualServiceMap ,
4142 filterServiceMapByHopDepth ,
4243 countVisibleServices ,
@@ -489,3 +490,139 @@ export const OPTION_B_EXPAND_COLLAPSE: StoryObj = {
489490 name : 'Option B — Expand / collapse hidden dependencies' ,
490491 render : ( ) => < ExpandCollapseExplorationPanel /> ,
491492} ;
493+
494+ function HighFanOutExplorationPanel ( ) {
495+ const [ neighborCount , setNeighborCount ] = useState ( 48 ) ;
496+ const [ viewMode , setViewMode ] = useState < 'full' | 'contextual' > ( 'full' ) ;
497+ const [ maxVisibleNodes , setMaxVisibleNodes ] = useState ( CONTEXTUAL_MAP_DEFAULT_MAX_VISIBLE_NODES ) ;
498+ const [ expandedNodeIds , setExpandedNodeIds ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
499+
500+ const {
501+ nodes : fullNodes ,
502+ edges : fullEdges ,
503+ focalServiceId,
504+ } = useMemo ( ( ) => createHighFanOutServiceMap ( neighborCount ) , [ neighborCount ] ) ;
505+
506+ const onExpand = useCallback ( ( nodeId : string ) => {
507+ setExpandedNodeIds ( ( prev ) => new Set ( prev ) . add ( nodeId ) ) ;
508+ } , [ ] ) ;
509+
510+ const onCollapse = useCallback ( ( nodeId : string ) => {
511+ setExpandedNodeIds ( ( prev ) => {
512+ const next = new Set ( prev ) ;
513+ next . delete ( nodeId ) ;
514+ return next ;
515+ } ) ;
516+ } , [ ] ) ;
517+
518+ const directNeighborCount = neighborCount ;
519+
520+ return (
521+ < div style = { { padding : 16 } } >
522+ < EuiTitle size = "s" >
523+ < h2 > High fan-out — many direct (1-hop) neighbors</ h2 >
524+ </ EuiTitle >
525+ < EuiText size = "s" color = "subdued" >
526+ < p >
527+ Star topology around < strong > { focalServiceId } </ strong > : { directNeighborCount } services at
528+ hop 1 (like a dense service-map tab). Use < strong > Full map</ strong > to match the global
529+ map; use < strong > Contextual</ strong > to see hop + node-cap behavior on the same dataset.
530+ </ p >
531+ </ EuiText >
532+
533+ < EuiSpacer size = "m" />
534+
535+ < EuiFlexGroup wrap alignItems = "center" gutterSize = "m" >
536+ < EuiFlexItem grow = { false } >
537+ < EuiButtonGroup
538+ legend = "View"
539+ options = { [
540+ { id : 'full' , label : 'Full map (screenshot-like)' } ,
541+ { id : 'contextual' , label : 'Contextual (Option B)' } ,
542+ ] }
543+ idSelected = { viewMode }
544+ onChange = { ( id ) => {
545+ setViewMode ( id as 'full' | 'contextual' ) ;
546+ setExpandedNodeIds ( new Set ( ) ) ;
547+ } }
548+ />
549+ </ EuiFlexItem >
550+ < EuiFlexItem grow = { false } >
551+ < EuiFieldNumber
552+ data-test-subj = "highFanOutNeighborCount"
553+ prepend = "Direct neighbors"
554+ value = { neighborCount }
555+ min = { 8 }
556+ max = { 80 }
557+ onChange = { ( e ) => {
558+ setNeighborCount ( e . target . valueAsNumber || 48 ) ;
559+ setExpandedNodeIds ( new Set ( ) ) ;
560+ } }
561+ />
562+ </ EuiFlexItem >
563+ { viewMode === 'contextual' && (
564+ < EuiFlexItem grow = { false } >
565+ < EuiFieldNumber
566+ data-test-subj = "highFanOutMaxVisibleNodes"
567+ prepend = "Max visible"
568+ value = { maxVisibleNodes }
569+ min = { 3 }
570+ max = { 30 }
571+ onChange = { ( e ) => {
572+ setMaxVisibleNodes (
573+ e . target . valueAsNumber || CONTEXTUAL_MAP_DEFAULT_MAX_VISIBLE_NODES
574+ ) ;
575+ setExpandedNodeIds ( new Set ( ) ) ;
576+ } }
577+ />
578+ </ EuiFlexItem >
579+ ) }
580+ < EuiFlexItem grow = { false } >
581+ < EuiBadge color = "hollow" > Focal: { focalServiceId } </ EuiBadge >
582+ </ EuiFlexItem >
583+ </ EuiFlexGroup >
584+
585+ < EuiSpacer size = "s" />
586+
587+ < EuiCallOut size = "s" title = "Dataset" iconType = "node" >
588+ < p >
589+ { directNeighborCount + 1 } nodes · { directNeighborCount } edges from focal · all neighbors
590+ are exactly 1 hop away
591+ </ p >
592+ </ EuiCallOut >
593+
594+ < EuiSpacer size = "m" />
595+
596+ { viewMode === 'full' ? (
597+ < ServiceMapGraph
598+ height = { getHeight ( ) }
599+ nodes = { fullNodes }
600+ edges = { fullEdges }
601+ environment = { defaultEnvironment }
602+ kuery = ""
603+ start = { defaultTimeRange . start }
604+ end = { defaultTimeRange . end }
605+ highlightedServiceName = { focalServiceId }
606+ />
607+ ) : (
608+ < CollapsibleServiceMapGraph
609+ height = { getHeight ( ) }
610+ nodes = { fullNodes }
611+ edges = { fullEdges }
612+ focalServiceId = { focalServiceId }
613+ baseMaxHops = { 1 }
614+ maxVisibleNodes = { maxVisibleNodes }
615+ expandedNodeIds = { expandedNodeIds }
616+ onExpand = { onExpand }
617+ onCollapse = { onCollapse }
618+ highlightedServiceName = { focalServiceId }
619+ />
620+ ) }
621+ </ div >
622+ ) ;
623+ }
624+
625+ export const HIGH_FAN_OUT_ONE_HOP_NEIGHBORS : StoryObj = {
626+ name : 'High fan-out — many direct 1-hop neighbors' ,
627+ render : ( ) => < HighFanOutExplorationPanel /> ,
628+ } ;
0 commit comments