Skip to content

[APM] Service map Popover and Node Details with react flow#251055

Merged
jennypavlova merged 11 commits intoelastic:mainfrom
jennypavlova:250209-apm-service-map-popover-and-node-details-with-react-flow
Feb 2, 2026
Merged

[APM] Service map Popover and Node Details with react flow#251055
jennypavlova merged 11 commits intoelastic:mainfrom
jennypavlova:250209-apm-service-map-popover-and-node-details-with-react-flow

Conversation

@jennypavlova
Copy link
Copy Markdown
Member

@jennypavlova jennypavlova commented Jan 30, 2026

Closes #250209

Summary

This PR adds all popovers in service map:

  • Service Node popover

    image
    • With Anomaly detection + dark theme:
    image
  • Dependecy Node popover

    image
  • Edge/Connection popover

    image
  • Group popover
    image

How to test

  • Verify that the Cytoscape service map is visible berfore adding the feature flag
  • Add xpack.apm.featureFlags.serviceMapUseReactFlow: true to the kibana.dev.yml ⚠️ Might change if [APM] Service map with react flow: Change from APM feature flags to platform #250783 is merged before this one ⚠️
  • Verify the nodes and edges are rendered and highlighted on click
  • Verify the nodes and edges are visible and clickable (all popovers should be visible for service, dependency, group, connection
  • Verify that the dark mode is supported

How to run the scout tests

  • Start the server with the React Flow feature flag enabled:
    node scripts/scout.js start-server --serverless=oblt --config-dir react_flow_service_map
    
  • Run the React Flow tests:
    npx playwright test --config=x-pack/solutions/observability/plugins/apm/test/scout_react_flow_service_map/ui/parallel.playwright.config.ts --grep=@svlOblt --project=local
    

@jennypavlova jennypavlova self-assigned this Jan 30, 2026
@jennypavlova jennypavlova requested a review from a team as a code owner January 30, 2026 12:45
@jennypavlova jennypavlova added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:obs-presentation Focus: APM UI, Infra UI, Hosts UI, Universal Profiling, Obs Overview and left Navigation labels Jan 30, 2026
@elasticmachine
Copy link
Copy Markdown
Contributor

Pinging @elastic/obs-presentation-team (Team:obs-presentation)

Copy link
Copy Markdown
Contributor

@crespocarlos crespocarlos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!! I found some small issues

Span group is not getting the span details correctly:

current new
image image

The popover stays open even after the focus leaves the node:

current

old_popover.mov

new

new_popover.mov

The popovers are also missing the \/

current new
image image

? `${selectedEdge.source} → ${selectedEdge.target}`
: selectedNodeId ?? '';

const trigger = <div style={{ width: 1, height: 1, visibility: 'hidden' }} />;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an aria-hidden here?

Suggested change
const trigger = <div style={{ width: 1, height: 1, visibility: 'hidden' }} />;
const trigger = <div style={{ width: 1, height: 1, visibility: 'hidden' }} aria-hidden="true"/>;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks. added ✅

Comment on lines +63 to +64
[SPAN_TYPE]: 'spanType' in nodeData ? nodeData.spanType : undefined,
spanType: 'spanType' in nodeData ? nodeData.spanType : undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can store 'spanType' in nodeData ? nodeData.spanType : undefined in a variable and reuse it?

also, do we need SPAN_TYPE and spanType

Comment on lines +57 to +66
/** Currently focused service name (for service-specific map) */
serviceName?: string;
/** Environment filter */
environment: Environment;
/** KQL filter */
kuery: string;
/** Start time */
start: string;
/** End time */
end: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need these comments?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed ✅

Comment on lines +148 to +149
setSelectedNodeForPopover(null);
setSelectedEdgeForPopover(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: why do we need to set both edge and node? Wouldn't edge be sufficient?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand the question: I think we should reset both by design if I understood the requirements correctly 🤔

excludeEdges,
});

const handleDiagnoseClick = () => setIsDiagnosticFlyoutOpen(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useCallback?

Comment on lines +40 to +41
if (nodeData.isService) {
const serviceData = nodeData as ServiceNodeData;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use type guards here? I think we already have one for this case, right?

Comment on lines +51 to +52
if ('isGrouped' in nodeData && nodeData.isGrouped) {
const groupedData = nodeData as GroupedNodeData;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use type guards here too

Comment on lines +126 to +127
const viewport = reactFlowInstance.getViewport();
const zoom = viewport.zoom;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention here to not recalculate the style when the viewport changes? Asking because if we need to reclaculate we probably need to pass the viewport as a dependency of this useMemo. We could probably use the useViewPort hook from react flow if we want to do that

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the look and fill if we keep the same position and don't recalculate the viewport also performance-wise is better. But if you see a benefit in doing so I am Ok to change it - just that it will have a "jumping" behaviour if we do so and now it feels smoother IMO (from a UX point of view, also using the buttons to zoom will close the flyout anyway and I also need to fix the other bug you found with the focus change when moving/dragging the map position) This is the current version:

zoom_sm.mov

const reactFlowInstance = useReactFlow();

const nodeData = selectedNode?.data;
const selectedNodeId = selectedNode?.id;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: selectedNodeId is only used once. we could use selectedNode?.id directly

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use it 2 times (line 203 and 213) 🤔

Comment on lines +117 to +118
stroke: isHighlighted ? primaryColor : defaultEdgeColor,
strokeWidth: isHighlighted ? HIGHLIGHTED_STROKE_WIDTH : DEFAULT_STROKE_WIDTH,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if edge.style has other attributes, we're losing them.

Suggested change
stroke: isHighlighted ? primaryColor : defaultEdgeColor,
strokeWidth: isHighlighted ? HIGHLIGHTED_STROKE_WIDTH : DEFAULT_STROKE_WIDTH,
...edge.style,
stroke: isHighlighted ? primaryColor : defaultEdgeColor,
strokeWidth: isHighlighted ? HIGHLIGHTED_STROKE_WIDTH : DEFAULT_STROKE_WIDTH,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thank you! Added ✅

@jennypavlova
Copy link
Copy Markdown
Member Author

@crespocarlos Thank you for reviewing! I am going through the comments and I have a QQ: Where do you see the arrow, on edge lite it is not visible (IMO it looks better without it and maybe it got removed but we can discuss if we need it. It could be a bug on edge that it is not visible - should we check with design, wdyt?)
image

@crespocarlos
Copy link
Copy Markdown
Contributor

QQ: Where do you see the arrow, on edge lite it is not visible (IMO it looks better without it and maybe it got removed but we can discuss if we need it. It could be a bug on edge that it is not visible - should we check with design, wdyt?)

I checked it in the Overview cluster. I don't see it on the edge either, but I don't remember if anything changed on the popover recently

Copy link
Copy Markdown
Member Author

@jennypavlova jennypavlova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crespocarlos Thanks you for the review! I also fixed the issue with moving the map not closing the popovers, good catch there!

map_move_fix.mov

( I am investigating the undefined (undefined) issue - I guess it's the key format difference of Cytoscape/React flow, I don't know how I missed that 🤦‍♀️ )

Comment on lines +117 to +118
stroke: isHighlighted ? primaryColor : defaultEdgeColor,
strokeWidth: isHighlighted ? HIGHLIGHTED_STROKE_WIDTH : DEFAULT_STROKE_WIDTH,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thank you! Added ✅

Comment on lines +126 to +127
const viewport = reactFlowInstance.getViewport();
const zoom = viewport.zoom;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the look and fill if we keep the same position and don't recalculate the viewport also performance-wise is better. But if you see a benefit in doing so I am Ok to change it - just that it will have a "jumping" behaviour if we do so and now it feels smoother IMO (from a UX point of view, also using the buttons to zoom will close the flyout anyway and I also need to fix the other bug you found with the focus change when moving/dragging the map position) This is the current version:

zoom_sm.mov

@jennypavlova
Copy link
Copy Markdown
Member Author

Fixed the group popover issue and update the description ✅

image

@smith
Copy link
Copy Markdown
Contributor

smith commented Jan 30, 2026

The popover doesn't close when it encounters a drag event:
CleanShot 2026-01-30 at 11 16 29

It probably should to match the zoom behavior and the old behavior.

Copy link
Copy Markdown
Contributor

@smith smith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good. Tried locally (didn't test anomalies or externals grouping.) See my note about drag behavior.

@jennypavlova
Copy link
Copy Markdown
Member Author

@smith Good catch, thanks! Fixed ✅:

fix_node_drag.mov

Copy link
Copy Markdown
Contributor

@crespocarlos crespocarlos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

@jennypavlova jennypavlova enabled auto-merge (squash) February 2, 2026 10:04
@jennypavlova jennypavlova merged commit 227d7d2 into elastic:main Feb 2, 2026
17 checks passed
@elasticmachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] Scout: [ observability / apm ] plugin / Can create, trigger and view an 'Error count' alert from service inventory
  • [job] [logs] Scout: [ observability / apm ] plugin / serverless-oblt - Alerts - Can create, trigger and view an 'Error count' alert from service inventory

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
apm 2358 2360 +2

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
apm 3.1MB 3.1MB +4.4KB

History

cc @jennypavlova

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:obs-presentation Focus: APM UI, Infra UI, Hosts UI, Universal Profiling, Obs Overview and left Navigation v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[APM] Service map Popover and Node Details with react flow

5 participants