Skip to content

Commit 57e29ea

Browse files
added highfan-out story, many 1 direct connection
1 parent be4d069 commit 57e29ea

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

x-pack/solutions/observability/plugins/apm/public/components/app/service_map/__stories__/service_contextual_map_exploration.stories.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
import {
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+
};

x-pack/solutions/observability/plugins/apm/public/components/app/service_map/__stories__/service_contextual_map_utils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,80 @@ export function createRichContextualServiceMap(): {
479479
export function createBranchingServiceMap(): ReturnType<typeof createRichContextualServiceMap> {
480480
return createRichContextualServiceMap();
481481
}
482+
483+
const FAN_OUT_AGENT_NAMES: AgentName[] = [
484+
'java',
485+
'nodejs',
486+
'go',
487+
'python',
488+
'dotnet',
489+
'ruby',
490+
'php',
491+
'rum-js',
492+
];
493+
494+
/**
495+
* Star topology: one focal service with many direct (1-hop) service neighbors.
496+
* Mimics dense production maps where a single service fans out to dozens of peers.
497+
*/
498+
export function createHighFanOutServiceMap(neighborCount = 48): {
499+
nodes: ServiceMapNode[];
500+
edges: ServiceMapEdge[];
501+
focalServiceId: string;
502+
} {
503+
const focalServiceId = 'product-page';
504+
505+
const mkService = (
506+
id: string,
507+
agentName: AgentName,
508+
extra?: Partial<ServiceNodeData>
509+
): ServiceMapNode => ({
510+
id,
511+
type: 'service',
512+
position: { x: 0, y: 0 },
513+
data: {
514+
id,
515+
label: id,
516+
isService: true,
517+
agentName,
518+
...extra,
519+
},
520+
});
521+
522+
const mkEdge = (source: string, target: string): ServiceMapEdge =>
523+
({
524+
id: `${source}~${target}`,
525+
source,
526+
target,
527+
data: {
528+
isBidirectional: false,
529+
sourceData: { id: source },
530+
targetData: { id: target },
531+
},
532+
type: 'default',
533+
style: { stroke: '#c8c8c8', strokeWidth: 1 },
534+
markerEnd: {
535+
type: MarkerType.ArrowClosed,
536+
width: 12,
537+
height: 12,
538+
color: '#c8c8c8',
539+
},
540+
} as ServiceMapEdge);
541+
542+
const nodes: ServiceMapNode[] = [mkService(focalServiceId, 'rum-js', { alertsCount: 1 })];
543+
const edges: ServiceMapEdge[] = [];
544+
545+
for (let i = 0; i < neighborCount; i++) {
546+
const neighborId = `downstream-${String(i + 1).padStart(2, '0')}`;
547+
const agentName = FAN_OUT_AGENT_NAMES[i % FAN_OUT_AGENT_NAMES.length];
548+
nodes.push(
549+
mkService(neighborId, agentName, {
550+
...(i % 7 === 0 ? { alertsCount: 1 + (i % 4) } : {}),
551+
...(i % 11 === 0 ? { sloStatus: 'degrading', sloCount: 1 } : {}),
552+
})
553+
);
554+
edges.push(mkEdge(focalServiceId, neighborId));
555+
}
556+
557+
return { nodes, edges, focalServiceId };
558+
}

0 commit comments

Comments
 (0)