Skip to content

Node-to-node volume heatmap + volume-coloured edges (#525)#542

Merged
NotYuSheng merged 3 commits into
mainfrom
feature/525-volume-heatmap-edges
Jul 18, 2026
Merged

Node-to-node volume heatmap + volume-coloured edges (#525)#542
NotYuSheng merged 3 commits into
mainfrom
feature/525-volume-heatmap-edges

Conversation

@NotYuSheng

@NotYuSheng NotYuSheng commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Closes #525.

What

Adds a pair-level volumetric view to the network diagram — the diagram shows who talks to whom, but nothing showed how much. A src×dst matrix is the right shape for "which pair is loudest".

Available in both analysis mode and the monitor snapshot dialog (Capture Timeline → a snapshot → Network Diagram tab).

Changes

  • Shared volume colour scale (utils/volumeColor.ts) — one log-scaled blue ramp, light/dark aware, replacing the two copy-pasted linear trafficColor helpers in ClusterGraph and CountryMapView (an acceptance criterion). Ramps validated against both surfaces; domain is fitted to the on-screen range so the full gradient lands on real values rather than a floor nothing reaches.
  • Node-to-Node Volume heatmap (VolumeHeatmap/) — src×dst matrix rendered to a canvas. The grid is O(N²) (500 hosts = 250k cells, which would freeze a DOM table) but sparse, so cost scales with conversation count, not N². Fits to the container, with a zoom control and fullscreen for dense captures. Axis labels are IPs (unique, unlike identity/device labels); clicking one opens host details.
  • Volume-coloured diagram edges — a "Color edges by: Protocol | Volume" toggle on NetworkGraph, sharing the heatmap's scale + a gradient legend. Switching repaints in place without re-running the layout. Resolves the protocol-vs-volume conflict deliberately (one channel at a time), per the issue.
  • Cross-filtering — click a cell to highlight that pair in the diagram; click a diagram node to emphasise its row/column.
  • Deletes MonitorNetworkDiagram.tsx — dead since feat: network monitor insights, node roles, subnets, annotations & per-snapshot details #321 replaced it with the snapshot-modal flow.

Design decisions (confirmed with the issue author)

One correction to the issue's premise: a cell and an edge can't mean the exact same number, because the diagram splits a pair into one edge per protocol while a cell is the pair total — they agree only for single-protocol pairs. Documented rather than papered over.

Verification

  • npx tsc --noEmit clean; new files lint clean (NetworkDiagramPage has 2 pre-existing graphCardRef ref-in-render errors, confirmed on main).
  • docker compose up -d --build succeeds; app serves and the feature is in the shipped bundle.
  • Colour scale + layout maths exercised against real backend data (skewed byte distributions, edge cases: zero/equal/inverted domains, 10→1000 hosts).

⚠️ Not verified by eye — no browser automation in this environment. The canvas rendering (devicePixelRatio crispness, tooltip positioning), fullscreen flex/z-index layering, and Escape precedence are confirmed by build + shipped-bundle inspection only, and warrant a manual pass before merge.

Docs updated: network-intelligence.rst (shared scale) and network-visualization.rst (heatmap, edge modes, both entry points).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added volume-based edge coloring to network diagrams, with protocol coloring remaining available.
    • Added a Node-to-Node Volume heatmap with zoom, fullscreen, tooltips, selection, and cross-filtering.
    • Added shared volume legends and consistent light/dark color scales across visualizations.
  • Documentation

    • Expanded guidance for edge color modes, heatmap interpretation, filtering, scaling, and interactions.

)

Adds a pair-level volumetric view to the network diagram, in both analysis
mode and the monitor snapshot dialog. The diagram shows topology; nothing
showed how *much* each pair talks. A matrix is the right shape for that.

- Shared volume colour scale (utils/volumeColor.ts): one log-scaled,
  perceptually-tuned blue ramp, light/dark aware, replacing the two
  copy-pasted linear `trafficColor` helpers in ClusterGraph and
  CountryMapView. Ramps validated against both surfaces. Domain is fitted
  to the on-screen range so the full gradient is spent on real values.
- VolumeHeatmap: src×dst matrix on a canvas (the grid is O(N²) but sparse,
  so cost scales with conversations, not N²). Fits to the container, with a
  zoom control and fullscreen for dense captures. Axis labels are IPs
  (unique, unlike identity labels); clicking one opens host details.
- Volume-coloured diagram edges via a "Color edges by: Protocol | Volume"
  toggle on NetworkGraph, sharing the heatmap's scale and a gradient legend.
  Switching repaints in place without re-running the layout.
- Wired into NetworkDiagramPage (analysis) and SnapshotDetailModal (the
  monitor Capture Timeline diagram), with cross-filter highlighting.
- Deletes MonitorNetworkDiagram.tsx, dead since #321 replaced it with the
  snapshot-modal flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@NotYuSheng, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: df81ee8f-dab5-472e-97dc-0b2bb6066d8d

📥 Commits

Reviewing files that changed from the base of the PR and between 62e97e8 and 32f0de7.

📒 Files selected for processing (1)
  • frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.css
📝 Walkthrough

Walkthrough

Adds a shared dark-mode logarithmic volume scale, volume-colored network edges, a node-to-node canvas heatmap, cross-filtering and fullscreen controls, integration across analysis and monitor diagrams, and expanded visualization documentation.

Changes

Volume visualization

Layer / File(s) Summary
Shared volume scale and theme handling
frontend/src/utils/*, frontend/src/components/intelligence/ClusterGraph/*
Shared logarithmic, light/dark-aware volume colors replace local traffic interpolation in cluster and country views.
Graph edge color modes and legend
frontend/src/components/network/NetworkGraph/*, frontend/src/components/network/VolumeLegend/*
Network edges support Protocol or Volume coloring, with filtered-byte range calculation, repainting without layout rebuilds, and a logarithmic legend.
Node-to-node volume heatmap
frontend/src/components/network/VolumeHeatmap/*
Adds aggregation, capped host axes, canvas rendering, zoom, labels, tooltips, selection, accessibility readouts, and fullscreen styling.
Diagram integration and cross-filtering
frontend/src/pages/NetworkDiagram/*, frontend/src/components/monitor/SnapshotDetailModal/*
Adds edge-color controls, heatmap panels, fullscreen handling, legends, pair selection, and graph highlighting to analysis and snapshot diagrams.
Visualization documentation
docs/features/network-intelligence.rst, docs/features/network-visualization.rst
Documents the shared scale, edge modes, heatmap aggregation, direction semantics, filtering, rendering, and interactions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • NotYuSheng/TracePcap issue 505 — Covers the shared logarithmic volume scale, volume-colored edges, paired node-to-node heatmap, and cross-filtering implemented here.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a node-to-node volume heatmap plus volume-colored edges.
Linked Issues check ✅ Passed The heatmap, volume edge coloring, shared scale, and monitor-mode integration all align with #525's requirements.
Out of Scope Changes check ✅ Passed No clearly unrelated changes are evident; the docs, shared utilities, and component updates all support the requested feature work.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request unifies traffic volume visualization across the application by introducing a shared, theme-aware logarithmic color scale utility (volumeColor.ts) and a useResolvedDark hook. These are integrated into cluster graphs, country maps, and network topology diagrams. The PR also adds a new VolumeHeatmap component for node-to-node volume matrices, a VolumeLegend component, and options to color diagram edges by volume, while removing the obsolete MonitorNetworkDiagram.tsx file. Review feedback highlights a duplication of CSS rules for .tp-heatmap-fullscreen-over-modal at the end of VolumeHeatmap.css that should be removed to improve maintainability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.css Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx`:
- Around line 341-346: Fit each intelligence volume color ramp to its displayed
positive range. In
frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx:341-346, add
the smallest positive cluster volume to the node data and pass it to volumeRatio
and makeVolumeColor. In
frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx:125-131,
compute separate smallest positive country and city volumes and pass the
appropriate minimum to makeVolumeColor.

In `@frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx`:
- Around line 124-126: Update the traffic legend in CountryMapView to avoid
claiming that dark blue represents more traffic, since the dark-mode ramp uses
brighter blue for higher volume. Use theme-neutral wording or the existing
shared gradient legend while leaving the color calculation unchanged.

In `@frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx`:
- Around line 272-291: Update the Escape handling in the SnapshotDetailModal
keydown handler to dismiss the selected NodeDetails first when
isHeatmapFullscreen and selectedNode are both active. Clear selectedNode before
exiting fullscreen, while preserving the existing fullscreen Escape behavior
when no node is selected and the snapshot-stepping guard afterward; follow the
precedence used by NetworkDiagramPage.

In `@frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx`:
- Around line 400-440: Replace the raw Bootstrap buttons in the heatmap controls
with the SGDS React Button component imported from `@govtechsg/sgds-react`. Update
the zoom-out, zoom-in, Fit, and Clear selection controls while preserving their
existing handlers, disabled states, labels, titles, and styling intent.
- Around line 100-107: Update the edge aggregation loop in VolumeHeatmap to
normalize each source/target pair as an unordered pair and aggregate its bytes,
packets, and conversations once. After aggregation, populate both directed cell
keys with the same totals, including reverse-direction cells even when no
reverse edge exists.
- Around line 446-458: Update the heatmap interaction around the canvas and its
handlers (`handleMove`, `handleClick`) to provide keyboard access: make the
control focusable, expose an appropriate grid/cell accessibility model, and
support arrow-key cell navigation plus Enter activation for inspecting or
selecting pairs and host labels. Preserve the existing mouse interactions while
keeping focus and active-cell state synchronized with the visual heatmap.

In `@frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx`:
- Around line 658-666: Add explicit aria-label values matching each icon-only
Button’s title and mark every nested decorative i element with
aria-hidden="true": update NetworkDiagramPage.tsx at lines 658-666 and 680, and
SnapshotDetailModal.tsx at lines 645-657 and 671.
- Around line 582-584: Explicitly associate each edge-color form label with its
select: in frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx lines
582-584, add htmlFor="nd-edge-color" to the Form.Label and id="nd-edge-color" to
the Form.Select; apply the corresponding htmlFor="sd-edge-color" and
id="sd-edge-color" pairing in
frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx
lines 548-550.

In `@frontend/src/utils/useResolvedDark.ts`:
- Around line 18-23: Update the system-mode branch in useResolvedDark so it
immediately synchronizes sysDark from mq.matches when the effect runs, before
subscribing to future change events. Preserve the existing handler and cleanup
behavior, ensuring re-entering system mode reflects the current OS preference
without waiting for another event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a02d01d3-ec27-47fa-8e40-4f3917572a47

📥 Commits

Reviewing files that changed from the base of the PR and between 325fc2b and 62e97e8.

📒 Files selected for processing (17)
  • docs/features/network-intelligence.rst
  • docs/features/network-visualization.rst
  • frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx
  • frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx
  • frontend/src/components/monitor/MonitorNetworkDiagram/MonitorNetworkDiagram.tsx
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx
  • frontend/src/components/network/NetworkGraph/NetworkGraph.tsx
  • frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.css
  • frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx
  • frontend/src/components/network/VolumeHeatmap/heatmapLayout.ts
  • frontend/src/components/network/VolumeHeatmap/index.ts
  • frontend/src/components/network/VolumeLegend/VolumeLegend.css
  • frontend/src/components/network/VolumeLegend/VolumeLegend.tsx
  • frontend/src/components/network/VolumeLegend/index.ts
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx
  • frontend/src/utils/useResolvedDark.ts
  • frontend/src/utils/volumeColor.ts
💤 Files with no reviewable changes (1)
  • frontend/src/components/monitor/MonitorNetworkDiagram/MonitorNetworkDiagram.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx`:
- Around line 341-346: Fit each intelligence volume color ramp to its displayed
positive range. In
frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx:341-346, add
the smallest positive cluster volume to the node data and pass it to volumeRatio
and makeVolumeColor. In
frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx:125-131,
compute separate smallest positive country and city volumes and pass the
appropriate minimum to makeVolumeColor.

In `@frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx`:
- Around line 124-126: Update the traffic legend in CountryMapView to avoid
claiming that dark blue represents more traffic, since the dark-mode ramp uses
brighter blue for higher volume. Use theme-neutral wording or the existing
shared gradient legend while leaving the color calculation unchanged.

In `@frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx`:
- Around line 272-291: Update the Escape handling in the SnapshotDetailModal
keydown handler to dismiss the selected NodeDetails first when
isHeatmapFullscreen and selectedNode are both active. Clear selectedNode before
exiting fullscreen, while preserving the existing fullscreen Escape behavior
when no node is selected and the snapshot-stepping guard afterward; follow the
precedence used by NetworkDiagramPage.

In `@frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx`:
- Around line 400-440: Replace the raw Bootstrap buttons in the heatmap controls
with the SGDS React Button component imported from `@govtechsg/sgds-react`. Update
the zoom-out, zoom-in, Fit, and Clear selection controls while preserving their
existing handlers, disabled states, labels, titles, and styling intent.
- Around line 100-107: Update the edge aggregation loop in VolumeHeatmap to
normalize each source/target pair as an unordered pair and aggregate its bytes,
packets, and conversations once. After aggregation, populate both directed cell
keys with the same totals, including reverse-direction cells even when no
reverse edge exists.
- Around line 446-458: Update the heatmap interaction around the canvas and its
handlers (`handleMove`, `handleClick`) to provide keyboard access: make the
control focusable, expose an appropriate grid/cell accessibility model, and
support arrow-key cell navigation plus Enter activation for inspecting or
selecting pairs and host labels. Preserve the existing mouse interactions while
keeping focus and active-cell state synchronized with the visual heatmap.

In `@frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx`:
- Around line 658-666: Add explicit aria-label values matching each icon-only
Button’s title and mark every nested decorative i element with
aria-hidden="true": update NetworkDiagramPage.tsx at lines 658-666 and 680, and
SnapshotDetailModal.tsx at lines 645-657 and 671.
- Around line 582-584: Explicitly associate each edge-color form label with its
select: in frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx lines
582-584, add htmlFor="nd-edge-color" to the Form.Label and id="nd-edge-color" to
the Form.Select; apply the corresponding htmlFor="sd-edge-color" and
id="sd-edge-color" pairing in
frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx
lines 548-550.

In `@frontend/src/utils/useResolvedDark.ts`:
- Around line 18-23: Update the system-mode branch in useResolvedDark so it
immediately synchronizes sysDark from mq.matches when the effect runs, before
subscribing to future change events. Preserve the existing handler and cleanup
behavior, ensuring re-entering system mode reflects the current OS preference
without waiting for another event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a02d01d3-ec27-47fa-8e40-4f3917572a47

📥 Commits

Reviewing files that changed from the base of the PR and between 325fc2b and 62e97e8.

📒 Files selected for processing (17)
  • docs/features/network-intelligence.rst
  • docs/features/network-visualization.rst
  • frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx
  • frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx
  • frontend/src/components/monitor/MonitorNetworkDiagram/MonitorNetworkDiagram.tsx
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx
  • frontend/src/components/network/NetworkGraph/NetworkGraph.tsx
  • frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.css
  • frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx
  • frontend/src/components/network/VolumeHeatmap/heatmapLayout.ts
  • frontend/src/components/network/VolumeHeatmap/index.ts
  • frontend/src/components/network/VolumeLegend/VolumeLegend.css
  • frontend/src/components/network/VolumeLegend/VolumeLegend.tsx
  • frontend/src/components/network/VolumeLegend/index.ts
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx
  • frontend/src/utils/useResolvedDark.ts
  • frontend/src/utils/volumeColor.ts
💤 Files with no reviewable changes (1)
  • frontend/src/components/monitor/MonitorNetworkDiagram/MonitorNetworkDiagram.tsx
🛑 Comments failed to post (9)
frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx (1)

341-346: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fit every intelligence volume ramp to its displayed positive range.

Both views accept the one-byte default minimum, compressing real traffic values into only part of the logarithmic ramp.

  • frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx#L341-L346: add the smallest positive cluster volume to node data and pass it to volumeRatio and makeVolumeColor.
  • frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx#L125-L131: compute separate smallest positive country and city volumes and pass them to makeVolumeColor.
📍 Affects 2 files
  • frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx#L341-L346 (this comment)
  • frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx#L125-L131
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx` around
lines 341 - 346, Fit each intelligence volume color ramp to its displayed
positive range. In
frontend/src/components/intelligence/ClusterGraph/ClusterGraph.tsx:341-346, add
the smallest positive cluster volume to the node data and pass it to volumeRatio
and makeVolumeColor. In
frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx:125-131,
compute separate smallest positive country and city volumes and pass the
appropriate minimum to makeVolumeColor.
frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx (1)

124-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the legend for the dark-mode ramp.

Higher volume becomes brighter blue in dark mode, so the existing “Dark blue = more traffic” legend states the opposite. Use theme-neutral wording or the shared gradient legend.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/intelligence/ClusterGraph/CountryMapView.tsx` around
lines 124 - 126, Update the traffic legend in CountryMapView to avoid claiming
that dark blue represents more traffic, since the dark-mode ramp uses brighter
blue for higher volume. Use theme-neutral wording or the existing shared
gradient legend while leaving the color calculation unchanged.
frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx (1)

272-291: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix Escape-key precedence to dismiss NodeDetails first.

When the heatmap is fullscreen and a node is selected, pressing Escape currently closes the heatmap fullscreen rather than dismissing the NodeDetails modal. Because this event handler runs in the capture phase and calls stopPropagation(), it starves the NodeDetails modal of the Escape key event, leaving the modal open over the collapsed view.

Clear the selectedNode first before exiting fullscreen, similar to the logic correctly implemented in NetworkDiagramPage.

🐛 Proposed fix
     const handler = (e: KeyboardEvent) => {
       // Heatmap fullscreen owns Escape while it is open — without this the key
       // falls through to the dialog and closes the whole thing instead of just
       // leaving fullscreen. Capture phase, so it runs before the modal's handler.
       if (e.key === 'Escape' && isHeatmapFullscreen) {
         e.preventDefault();
         e.stopPropagation();
+        if (selectedNode) {
+          setSelectedNode(null);
+          return;
+        }
         setIsHeatmapFullscreen(false);
         return;
       }
       // Snapshot stepping would be disorienting while the matrix is fullscreen.
       if (isHeatmapFullscreen) return;
       if (e.key === 'ArrowLeft' && diagramIndex > 0) {
         setDiagramSnapshotId(sorted[diagramIndex - 1].id);
       } else if (e.key === 'ArrowRight' && diagramIndex < sorted.length - 1) {
         setDiagramSnapshotId(sorted[diagramIndex + 1].id);
       }
     };
     window.addEventListener('keydown', handler, true);
     return () => window.removeEventListener('keydown', handler, true);
-  }, [activeTab, diagramIndex, sorted, isHeatmapFullscreen]);
+  }, [activeTab, diagramIndex, sorted, isHeatmapFullscreen, selectedNode]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      // Heatmap fullscreen owns Escape while it is open — without this the key
      // falls through to the dialog and closes the whole thing instead of just
      // leaving fullscreen. Capture phase, so it runs before the modal's handler.
      if (e.key === 'Escape' && isHeatmapFullscreen) {
        e.preventDefault();
        e.stopPropagation();
        if (selectedNode) {
          setSelectedNode(null);
          return;
        }
        setIsHeatmapFullscreen(false);
        return;
      }
      // Snapshot stepping would be disorienting while the matrix is fullscreen.
      if (isHeatmapFullscreen) return;
      if (e.key === 'ArrowLeft' && diagramIndex > 0) {
        setDiagramSnapshotId(sorted[diagramIndex - 1].id);
      } else if (e.key === 'ArrowRight' && diagramIndex < sorted.length - 1) {
        setDiagramSnapshotId(sorted[diagramIndex + 1].id);
      }
    };
    window.addEventListener('keydown', handler, true);
    return () => window.removeEventListener('keydown', handler, true);
  }, [activeTab, diagramIndex, sorted, isHeatmapFullscreen, selectedNode]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx`
around lines 272 - 291, Update the Escape handling in the SnapshotDetailModal
keydown handler to dismiss the selected NodeDetails first when
isHeatmapFullscreen and selectedNode are both active. Clear selectedNode before
exiting fullscreen, while preserving the existing fullscreen Escape behavior
when no node is selected and the snapshot-stepping guard afterward; follow the
precedence used by NetworkDiagramPage.
frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx (3)

100-107: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mirror pair totals across both matrix directions.

Only source → target is populated, leaving the reverse cell empty even though totalBytes has no per-direction breakdown. Normalize each unordered pair, aggregate it once, then expose the same total at both directed cell keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx` around lines
100 - 107, Update the edge aggregation loop in VolumeHeatmap to normalize each
source/target pair as an unordered pair and aggregate its bytes, packets, and
conversations once. After aggregation, populate both directed cell keys with the
same totals, including reverse-direction cells even when no reverse edge exists.

400-440: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use SGDS buttons for the heatmap controls.

Replace the raw Bootstrap zoom, fit, and clear buttons with Button from @govtechsg/sgds-react.

As per coding guidelines, “Always use SGDS components from @govtechsg/sgds-react before building custom UI components.” <coding_guidelines>

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx` around lines
400 - 440, Replace the raw Bootstrap buttons in the heatmap controls with the
SGDS React Button component imported from `@govtechsg/sgds-react`. Update the
zoom-out, zoom-in, Fit, and Clear selection controls while preserving their
existing handlers, disabled states, labels, titles, and styling intent.

Source: Coding guidelines


446-458: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Provide keyboard interaction for the heatmap.

The canvas is a non-focusable role="img" with mouse-only handlers, so keyboard users cannot inspect cells, select pairs, or activate host labels. Add a focusable grid interaction model with arrow keys and Enter, or an equivalent accessible control/list representation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/network/VolumeHeatmap/VolumeHeatmap.tsx` around lines
446 - 458, Update the heatmap interaction around the canvas and its handlers
(`handleMove`, `handleClick`) to provide keyboard access: make the control
focusable, expose an appropriate grid/cell accessibility model, and support
arrow-key cell navigation plus Enter activation for inspecting or selecting
pairs and host labels. Preserve the existing mouse interactions while keeping
focus and active-cell state synchronized with the visual heatmap.
frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx (2)

582-584: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Explicitly associate the form label with the select element.

Screen readers may not correctly announce the label for the <Form.Select> because it is not explicitly linked to the <Form.Label>. Provide an id to the select element and reference it with the htmlFor attribute on the label.

  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L582-L584: Add htmlFor="nd-edge-color" to the <Form.Label> and id="nd-edge-color" to the <Form.Select>.
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L548-L550: Add htmlFor="sd-edge-color" to the <Form.Label> and id="sd-edge-color" to the <Form.Select>.
📍 Affects 2 files
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L582-L584 (this comment)
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L548-L550
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx` around lines 582 -
584, Explicitly associate each edge-color form label with its select: in
frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx lines 582-584, add
htmlFor="nd-edge-color" to the Form.Label and id="nd-edge-color" to the
Form.Select; apply the corresponding htmlFor="sd-edge-color" and
id="sd-edge-color" pairing in
frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx
lines 548-550.

658-666: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ensure icon buttons have an accessible name and decorative icons are hidden.

Icon-only buttons naturally rely on their title attribute for pointer tooltips, but should also include an explicit aria-label for reliable screen reader announcement. Additionally, decorative icons inside buttons should be marked with aria-hidden="true". Based on learnings, interactive elements using decorative icons should provide proper accessible labels and hide the icons from assistive technologies.

  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L658-L666: Add aria-label to the <Button> (matching its title) and aria-hidden="true" to the nested <i> element.
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L680-L680: Add aria-hidden="true" to the <i> element.
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L645-L657: Add aria-label to the <Button> (matching its title) and aria-hidden="true" to the nested <i> element.
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L671-L671: Add aria-hidden="true" to the <i> element.
📍 Affects 2 files
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L658-L666 (this comment)
  • frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx#L680-L680
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L645-L657
  • frontend/src/components/monitor/SnapshotDetailModal/SnapshotDetailModal.tsx#L671-L671
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/NetworkDiagram/NetworkDiagramPage.tsx` around lines 658 -
666, Add explicit aria-label values matching each icon-only Button’s title and
mark every nested decorative i element with aria-hidden="true": update
NetworkDiagramPage.tsx at lines 658-666 and 680, and SnapshotDetailModal.tsx at
lines 645-657 and 671.

Source: Learnings

frontend/src/utils/useResolvedDark.ts (1)

18-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Synchronize the OS preference when re-entering system mode.

If the OS theme changes while light or dark is selected, switching back to system returns stale sysDark until another media-query event occurs.

Proposed fix
   useEffect(() => {
     if (themeMode !== 'system') return;
     const mq = window.matchMedia('(prefers-color-scheme: dark)');
+    setSysDark(mq.matches);
     const handler = (e: MediaQueryListEvent) => setSysDark(e.matches);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  useEffect(() => {
    if (themeMode !== 'system') return;
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    setSysDark(mq.matches);
    const handler = (e: MediaQueryListEvent) => setSysDark(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/useResolvedDark.ts` around lines 18 - 23, Update the
system-mode branch in useResolvedDark so it immediately synchronizes sysDark
from mq.matches when the effect runs, before subscribing to future change
events. Preserve the existing handler and cleanup behavior, ensuring re-entering
system mode reflects the current OS preference without waiting for another
event.

NotYuSheng and others added 2 commits July 19, 2026 02:27
The .tp-heatmap-fullscreen-over-modal rules were refactored into a shared
.nd-css-fullscreen block but the original standalone block was left at the
end of the file. Per gemini-code-assist review on #542.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NotYuSheng
NotYuSheng merged commit fbc76fb into main Jul 18, 2026
4 checks passed
@NotYuSheng
NotYuSheng deleted the feature/525-volume-heatmap-edges branch July 18, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Node-to-node volumetric heatmap paired with the network diagram, and volume-coloured edges (incl. monitor mode)

1 participant