Skip to content

feat(app): dashboard section navigation aids (TOC, view options, deep links)#2330

Open
teeohhem wants to merge 4 commits into
mainfrom
dashboard-toc-navigation
Open

feat(app): dashboard section navigation aids (TOC, view options, deep links)#2330
teeohhem wants to merge 4 commits into
mainfrom
dashboard-toc-navigation

Conversation

@teeohhem
Copy link
Copy Markdown
Contributor

@teeohhem teeohhem commented May 22, 2026

Summary

Adds three opt-in dashboard navigation aids for dashboards that have grown past comfortable scroll length. All three are off by default — existing dashboards look unchanged for users who never open the new menu.

  1. Hash anchors per section. Each DashboardContainer renders with id="container-<id>", and hovering a section header reveals a "copy link" icon that writes <current-url>#container-<id> to the clipboard (via the shared @/utils/clipboard helper). Loading the page with such a hash auto-scrolls to that section, auto-expanding it first.
  2. View-options menu in the toolbar. "Collapse all sections" / "Expand all sections" / "Show table of contents". Batch collapse/expand reuses the existing URL-synced collapse state (?collapsed=…&expanded=…), so a compact view is shareable like any other dashboard state.
  3. Sticky in-flow TOC rail. When toggled on, a 180px column sits next to the dashboard content as a Flex sibling (no overlay, tiles reflow), with one entry per section, scrollspy-driven active highlighting, and click-to-jump that also auto-expands. Hidden below the md breakpoint. Visibility is a per-user preference in useUserPreferences.

Core logic lives in src/hooks/useDashboardSectionNav.ts (scrollToContainer, copySectionLink, collapseAll, expandAll). New components are DashboardTOC and DashboardViewOptions. DashboardContainer gained an optional onCopyLink prop and a stable id attribute on its root.

Screenshots or video

Before
image

After

dashboardtoc.mp4

How to test on Vercel preview

Preview routes: `/dashboards/[id]` (any existing dashboard with 4+ sections)

Steps:

  1. Open a dashboard with at least four sections (group containers).
  2. Click the view-options icon in the toolbar (between "Edit Filters" and "Run"); verify the menu shows "Collapse all sections", "Expand all sections", and "Show table of contents".
  3. Click "Collapse all sections" — verify every section collapses to its header and the URL gains `?collapsed=…`.
  4. Click "Show table of contents" — verify a sticky rail appears on the right with one entry per section.
  5. Click any TOC entry — verify the page smooth-scrolls to that section and auto-expands it if collapsed.
  6. Hover any section header — verify a small link icon appears. Click it — verify the "Link copied" notification, paste the URL in a new tab, and verify the page loads and scrolls to that section.

References


Reviewer notes (pre-push fanout findings)

Ran `ce-correctness`, `ce-maintainability`, `ce-adversarial`, `ce-kieran-typescript`, and `ce-testing` reviewers in parallel before pushing. Triaged each finding as introduced/amplified/pre-existing.

Fixed before push (P1 / P2):

  • Hash-on-load effect now keys on `dashboard.id` (not a binary ref), so SPA navigation between dashboards re-arms the deep-link scroll, and the initial scroll is gated on containers being loaded.
  • `scrollToContainer` now uses double-rAF before measuring, so auto-expand-then-scroll on a collapsed section lands correctly after React commit + tile layout.
  • TOC label uses `container.title` for multi-tab containers (the tab bar replaces a single-title header in that case) and falls back to the first tab's title for single-tab containers.

Known follow-ups (P2, not fixed in this PR):

  • `useDashboardSectionNav` and `DBDashboardPage` both subscribe to `useQueryState('collapsed' | 'expanded')`. nuqs reads stay in sync, but consolidating the URL-collapse plumbing in one place would prevent future drift.
  • TOC entry hover styling uses inline `style` + `onMouseEnter/Leave`. A CSS module keyed on `data-active` would be cleaner and matches the convention used elsewhere in the repo.
  • `copySectionLink` includes the full current `window.location.search` in the copied URL — that intentionally captures the recipient's filter context, but means transient params (`highlightedTileId`, debug params) tag along. Worth a follow-up to filter.
  • `useScrollSpy` uses a global `[id^="container-"]` selector. Tightening to a scoped ref would be more robust against future ID collisions or drag-overlay clones.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: 2b6e5ee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 22, 2026 4:42pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label May 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 590 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 6
  • Production lines changed: 590 (+ 454 in test files, excluded from tier calculation)
  • Branch: dashboard-toc-navigation
  • Author: teeohhem

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

E2E Test Results

All tests passed • 177 passed • 3 skipped • 1279s

Status Count
✅ Passed 177
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

Deep Review

✅ No critical issues found.

The diff adds three opt-in navigation aids behind feature flags / per-user preferences (tocVisible defaults false, onCopyLink is opt-in, view-options menu gated on containers.length > 0), so the "do nothing different by default" claim holds. Hook logic in useDashboardSectionNav.ts is covered by 9 unit tests including the rAF-driven scroll path and the clipboard success/failure branches, DashboardContainer's new id and copy-link wiring are covered for both single-tab and multi-tab headers, and the URL-state plumbing reuses the existing parseAsArrayOf(parseAsString) keys without changing their semantics. WidthProvider(RGL) already observes parent resizes, so the new Flex wrapper around the tile grid does not regress react-grid-layout when the TOC sibling appears.

🔵 P3 nitpicks (5)
  • packages/app/src/hooks/useDashboardSectionNav.ts:52scrollToContainer unconditionally calls expandContainer, which appends the target id to the URL expanded param even when the section is already expanded; the URL accumulates redundant entries on every TOC click or deep-link jump.
    • Fix: Skip the setUrlExpandedIds write when the id is already present and not in the collapsed set, or pass an "already expanded" hint from the caller.
  • packages/app/src/DBDashboardPage.tsx:1746 — The hash-on-load effect depends on the full dashboard object, so it re-attaches the hashchange listener on every dashboard refetch even though it only reads dashboard.id and dashboard.containers.
    • Fix: Narrow the dependency array to [dashboard?.id, dashboard?.containers, scrollToContainer] to avoid churn on unrelated dashboard field updates.
  • packages/app/src/DBDashboardPage.tsx:1757 — The hash regex captures (.+) and compares the raw fragment against c.id with no decodeURIComponent, so a deep link to an id with URL-unsafe characters would silently fail the existence check.
    • Fix: Wrap the captured group in decodeURIComponent before the some check; do the symmetric encodeURIComponent in copySectionLink.
  • packages/app/src/components/DashboardTOC.tsx:25containerSignature joins ids with |, so two id sets that differ only by |-vs-empty would produce equal signatures and skip the reinitialize call.
    • Fix: Use JSON.stringify(containers.map(c => c.id)) or a delimiter known to be illegal in ids.
  • packages/app/src/hooks/__tests__/useDashboardSectionNav.test.tsx:131 — The requestAnimationFrame spy invokes its callback synchronously, so the assertion passes whether the hook calls rAF once, twice, or zero times; the test does not actually defend the double-rAF contract that justifies the comment in the hook.
    • Fix: Count rAF invocations or use a manual scheduler that lets the test assert callbacks run in two distinct frames before scrollIntoView fires.

Reviewers (8): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, adversarial, agent-native.

Testing gaps:

  • No integration test exercises the Flex + tocVisible layout path in DBDashboardPage; the TOC sidebar render and the tocContainers label-fallback memo (tabs.length >= 2 ? c.title : tabs[0]?.title ?? c.title) are only covered indirectly.
  • The hash-on-load effect (initial-scroll re-arm on dashboard.id change, listener cleanup) has no direct test; current coverage relies on the hook's scrollToContainer test plus the regex inline in the page.

teeohhem added 4 commits May 22, 2026 12:33
Adds three opt-in helpers for navigating dashboards with many sections,
all off by default so existing dashboards look unchanged.

- Hash anchors per section: each DashboardContainer renders with
  id="container-<id>". Hovering a section header reveals a "copy link"
  icon that copies the current URL plus a #container-<id> fragment
  (using the shared @/utils/clipboard helper). Loading the page with
  such a hash auto-scrolls to that section, auto-expanding it first.
- View-options menu in the dashboard toolbar: "Collapse all sections"
  / "Expand all sections" / "Show table of contents" toggle. Batch
  collapse/expand reuse the existing URL-synced collapse state so the
  resulting view is shareable.
- Sticky in-flow TOC rail: when enabled via the view-options toggle, a
  180px column renders beside the dashboard content (Flex sibling, not
  an overlay) with one entry per section. useScrollSpy highlights the
  active section as the user scrolls; click jumps. Hidden below the md
  breakpoint. The "tocVisible" preference is per-user in localStorage.

Core hook lives in src/hooks/useDashboardSectionNav.ts and exposes
scrollToContainer, copySectionLink, collapseAll, and expandAll.
The menu's items (collapse-all / expand-all / show TOC) are all
section-scoped and behave as no-ops on dashboards with zero containers.
Hide the toolbar entry-point in that case so it does not surface
unhelpful UI on sectionless dashboards. The TOC itself already
short-circuits when containers is empty.
DBDashboardPage owned a subscription on the 'collapsed' and 'expanded'
URL params via useQueryState; useDashboardSectionNav independently
subscribed to the same two keys. The hook now receives setters from
the caller instead so DBDashboardPage is the sole subscriber per key.

Eliminates a potential interaction between concurrent nuqs subscribers
during initial hydration on the saved dashboard route, where the
expected post-hydration re-render to populate router.query.dashboardId
could otherwise be delayed. Behaviour of collapseAll / expandAll /
scrollToContainer is unchanged; tests updated to inject the setters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant