Skip to content

feat(app): hover hint and native link affordance for dashboard table tile row click#2321

Open
alex-fedotyev wants to merge 3 commits into
mainfrom
alex/table-tile-onclick-hover-hint
Open

feat(app): hover hint and native link affordance for dashboard table tile row click#2321
alex-fedotyev wants to merge 3 commits into
mainfrom
alex/table-tile-onclick-hover-hint

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 20, 2026

Summary

When a dashboard table tile is configured with an onClick action (#2140, #2141, #2146, #2148), the row click destination is now discoverable before the user commits to the click.

What's new for users:

  • Hover hint. After ~250ms of hover, a small card above the row reads Search HyperDX Logs or Open dashboard "API Latency Drilldown" (or generic Open in search / Open dashboard when the target is templated or the named source / dashboard no longer exists).
  • Native browser behaviors. The cell wrapper is now a real <a href>. Cmd-click opens the destination in a new tab, middle-click opens it in a new tab, right-click shows the browser context menu with "Open in New Tab" and "Copy Link Address", and the destination URL appears in the status bar on hover.
  • Keyboard navigation. Tab focuses the first clickable cell with a visible focus ring; Enter activates it.

Screenshots

Search action: hovering a row reveals the resolved source name.

Light Dark
Search onClick hover hint, light theme Search onClick hover hint, dark theme

Dashboard action: hovering a row reveals the resolved dashboard name.

Light Dark
Dashboard onClick hover hint, light theme Dashboard onClick hover hint, dark theme

How the implementation changes:

  • packages/app/src/HDXMultiSeriesTableChart.tsx: cell wrapper goes from <div role="link" tabIndex={0}> plus manual onClick / onAuxClick / onMouseDown / onKeyDown handlers to a Next.js <Link href> with prefetch={false}. The HoverCard wraps the whole <tr> at the row body level so the hint position stays stable as the cursor moves across cells; cell-level mounting would flicker per column. Rows whose templates fail to resolve render as a real <button type="button"> (not <a href="#">) so cmd-click / middle-click / right-click "Open in New Tab" can't silently open a meaningless new tab against a # fragment.
  • packages/app/src/components/DBTableChart.tsx: drops the parent onRowClick cmd/middle-click branching; threads new getRowAction (or legacy getRowSearchLink) into Table.
  • packages/common-utils/src/core/linkUrlBuilder.ts: new describeOnClick({ onClick, sourceNamesById, dashboardNamesById }): string helper returns the one-line hint, with an exhaustiveness check on the OnClick discriminator.
  • packages/app/src/hooks/useOnClickLinkBuilder.ts: also builds Map<id, name> for sources and dashboards, computes the row-independent description once, and returns a per-row resolver of shape { url, description, onClickError? }. Per-row results are memoized internally (WeakMap) so cells sharing a row don't rerun handlebars rendering. When a row's templates fail to resolve, url is null and onClickError fires the existing Mantine "Link error" toast on click.
  • Focus ring uses the shared styles/focus.module.scss focusRing style.
  • Cell wrappers carry data-testid="dashboard-table-row-action" on both branches; the e2e page object resolves by testid so future inline column links don't steal the click.
  • The legacy getRowSearchLink drilldown path (used outside dashboards, e.g. the services dashboard) renders as a plain <Link href> without a HoverCard so behavior there is unchanged.

Test plan

  • make ci-lint clean across all 3 workspaces.
  • make ci-unit clean. New HDXMultiSeriesTableChart component test covers both success and failure branches end to end: anchor / button shape, onClickError wiring, row-level HoverCard hint visibility, legacy getRowSearchLink fallback.
  • describeOnClick unit tests cover all 6 OnClickSchema discriminator + name-lookup combinations.
  • useOnClickLinkBuilder hook tests cover: no onClick configured returns null, resolved source name in description, resolved dashboard name in description, row resolution failure encodes as url: null + click handler fires notification, WeakMap caching shares results across cells of the same row.
  • knip clean (no new unused exports).
  • UI verified in the dev stack (screenshots above). Status bar shows the destination URL on the success branch; failure branch (a <button>) shows no status bar URL and clicking fires the existing red Link error toast. Right-click on a success anchor shows the native context menu; cmd-click and middle-click open in a new tab.
  • Both light and dark theme verified.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 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 12:33am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 20, 2026

🦋 Changeset detected

Latest commit: 19c0204

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

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Patch
@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

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

github-actions Bot commented May 20, 2026

🟡 Tier 3 — Standard

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

Why this tier:

  • Diff size: 357 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches frontend (packages/app) + shared utils (packages/common-utils)

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

Stats
  • Production files changed: 5
  • Production lines changed: 357 (+ 423 in test files, excluded from tier calculation)
  • Branch: alex/table-tile-onclick-hover-hint
  • Author: alex-fedotyev

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 20, 2026

E2E Test Results

All tests passed • 178 passed • 3 skipped • 1195s

Status Count
✅ Passed 178
❌ Failed 0
⚠️ Flaky 4
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Deep Review

🟡 P2 -- recommended

  • packages/app/src/HDXMultiSeriesTableChart.tsx:206 -- Failure-row <button> only wires onClick, so a middle-click (button 1) fires onAuxClick and silently no-ops, while the adjacent success-row <a> opens in a new tab; the pre-diff <div role="link"> explicitly handled onAuxClick button-1, so this is a regression on the broken-row branch.
    • Fix: Add an onAuxClick handler on the failure <button> that forwards button-1 events to action.onClickError so middle-click still surfaces the link-error toast.
    • adversarial
🔵 P3 nitpicks (8)
  • packages/common-utils/src/core/linkUrlBuilder.ts:283 -- describeOnClick ends with return _exhaustive typed never, so if a future OnClick discriminator is added without updating this helper the hover hint silently renders the stringified object instead of failing loudly.

    • Fix: Replace return _exhaustive with throw new Error('unhandled OnClick variant') (or a safe 'Open link' fallback) so unhandled variants surface immediately.
    • correctness, kieran-typescript
  • packages/app/src/HDXMultiSeriesTableChart.tsx:391 -- Tooltip.Floating clones the <tr> and injects its own ref alongside the <tr>'s existing ref={rowVirtualizer.measureElement}; Mantine v9 merges refs via useMergedRef, but the composition is unverified for this combination and a silent override would degrade only success rows to the 58px estimateSize fallback while failure rows keep accurate measurement.

    • Fix: Smoke-test a tile with multi-line cell content (scroll, recycle, mixed success/failure rows) and lock the assumption with a test that confirms measureElement fires against the live DOM node after Mantine's clone.
    • correctness, adversarial, julik-frontend-races
  • packages/app/src/hooks/useOnClickLinkBuilder.ts:128 -- The notification id is the per-row errorMessage, so two failure rows producing different messages (e.g. Row has no column X vs Row has no column Y) stack as two toasts rather than deduping into one.

    • Fix: Use a stable id such as 'dashboard-table-link-error' and let the latest message replace the prior one in place.
    • adversarial
  • packages/app/styles/focus.module.scss:23 -- .cellButton:focus-visible duplicates the .focusRing:focus-visible outline/offset/radius rules verbatim while the failure-row <button> already carries focusRing via cx(...), so any future change to the focus token in one block but not the other silently drifts the focus ring.

    • Fix: Delete the duplicate .cellButton:focus-visible block and rely on the focusRing class the button already carries, or factor the outline rules into a shared mixin.
    • maintainability, julik-frontend-races
  • packages/app/src/hooks/useOnClickLinkBuilder.ts:28 -- RowAction.onClickError is typed React.MouseEvent<HTMLButtonElement>, leaking a specific DOM element choice into a resolver that otherwise has zero DOM dependency; the handler only calls preventDefault + notifications.show, so the narrow type forces callers into a <button> or a cast.

    • Fix: Widen the parameter to React.MouseEvent<HTMLElement> (or React.SyntheticEvent) so the resolver stops dictating the cell wrapper's element type.
    • maintainability, kieran-typescript
  • packages/app/src/HDXMultiSeriesTableChart.tsx:414 -- Failure rows render visually identical to success rows but with no hover hint, so users cannot distinguish a row that will navigate from a row that will fire an error toast until they click; the current code comment acknowledges showing the success hint would mislead, but provides no positive failure affordance either.

    • Fix: Render a distinct failure hint (e.g. Link unavailable -- click for details) or apply a muted style on failure rows so the asymmetric click outcome is discoverable before commit.
    • adversarial
  • packages/app/src/HDXMultiSeriesTableChart.tsx:185 -- Tab focus on a success row lands on an <a> (Enter activates, Space scrolls the page) while focus on a failure row lands on a <button> (Enter and Space both activate), so a keyboard user pressing Space to scroll gets unexpected toasts when focus happens to be on a broken row.

    • Fix: Either drop failure rows from the tab order with tabIndex={-1} or normalize the keyboard activation contract across both wrappers.
    • adversarial
  • packages/app/src/HDXMultiSeriesTableChart.tsx:388 -- The iteration-root Tooltip.Floating already sets key={virtualRow.key} while the inner <tr> also sets key={virtualRow.key}; the inner key is dead and obscures which element React actually reconciles.

    • Fix: Drop key from the inner <tr> and keep it only on the element returned from items.map.
    • julik-frontend-races

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

Testing gaps:

  • No unit test covers the prop precedence in DBTableChart.tsx when both getRowAction and getRowSearchLink resolve to a URL -- the getRowAction ? undefined : getRowSearchLink ternary could silently invert without any test failing.
  • No unit or e2e test exercises cmd-click, middle-click, right-click "Open in New Tab", or "Copy Link Address" on the success-row anchor; the headline interaction claims rely entirely on native <a> semantics with no executable guard.
  • No test asserts that the row-level Tooltip.Floating is suppressed when rowAction.url === null; a regression removing the && rowAction.url guard would render a misleading hint on broken rows undetected.
  • No test verifies that the WeakMap cache is rebuilt when the memo deps (onClick, sources, dashboards, dateRange) change -- only intra-render referential equality is currently asserted.
  • The virtualizer is fully stubbed in HDXMultiSeriesTableChart.test.tsx, so production interactions between Tooltip.Floating, rowVirtualizer.measureElement, and scroll-driven row recycling are not exercised at any layer.
  • No test exercises the failure-row keyboard activation path (focusStyles.cellButton + <button type="button"> Enter/Space behavior) or asserts the failure cell has no link-like role attribute.
  • The negative-hint assertion uses a 250 ms wall-clock setTimeout rather than a deterministic "tooltip never mounted" check -- a slow CI runner could exit before the tooltip would have appeared, hiding a regression.
  • learnings-researcher found zero relevant prior solutions in docs/solutions/ for any of: Mantine tooltip + virtualized rows, Next.js <Link> vs role="link" for keyboard/aux-click parity, WeakMap caching in React hooks, or Playwright testid-vs-role selector strategy -- worth a /ce-compound capture after this lands.

@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Pushed aa16b21d addressing the deep-review findings.

P0 fix: failure rows now render as a real <button type="button"> instead of <a href="#">. Cmd-click, middle-click, and right-click "Open in New Tab" no longer offer a meaningless # target; the error toast still fires on left-click.

P2 fixes:

  • HoverCard now wraps the whole <tr> once at the row body level instead of one HoverCard per cell. Hint position stays stable across horizontal cursor movement; no per-column flicker.
  • getRowAction is computed once per row; the hook caches by row reference (WeakMap) so per-cell calls share the result. No more handlebars + URLSearchParams build per cell per render.
  • <Link> now uses prefetch={false} so virtualized scroll doesn't trigger an N-row prefetch storm against /search? and /dashboards/ routes.
  • New HDXMultiSeriesTableChart component test covers both branches end to end: success anchor, failure button + onClickError wiring, row-level HoverCard hint visibility, legacy getRowSearchLink fallback.

Smaller cleanups picked up along the way:

  • describeOnClick does an explicit exhaustiveness check on the OnClick discriminator (adds a third variant -> type error).
  • onClickError is typed React.MouseEvent<HTMLButtonElement>.
  • Cell wrappers carry data-testid="dashboard-table-row-action" on both branches; the e2e page object resolves by testid.
  • Hook test asserts URL params via URLSearchParams instead of position-coupled regex.

Screenshots in the description show the hover hint working on both search and dashboard targets, in light and dark theme.

@pulpdrew
Copy link
Copy Markdown
Contributor

Given a case where a link is invalid for some rows (typically using a templated dashboard name), the invalid rows are rendered as buttons with different styles than the valid rows which are links. Also odd that in that case the tooltip still shows up

Screenshot 2026-05-21 at 3 22 28 PM

IMO the placement of the tooltip is odd, it should be right where the cursor is instead of in the middle of the row

Screenshot 2026-05-21 at 3 24 33 PM

pulpdrew
pulpdrew previously approved these changes May 21, 2026
Copy link
Copy Markdown
Contributor

@pulpdrew pulpdrew left a comment

Choose a reason for hiding this comment

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

LGTM, with a few suggestions

Comment on lines +200 to +201
// notification toast on left-click; the proper "muted row
// + warning icon" preempt state is tracked as AC8.
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 - comments tied to specific prompts are just clutter

Suggested change
// notification toast on left-click; the proper "muted row
// + warning icon" preempt state is tracked as AC8.
// notification toast on left-click;

@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Done in dd50d56 (removed the internal AC tracking note from the button fallback comment, per your suggestion).

Alex Fedotyev added 2 commits May 22, 2026 00:19
…tile row click

When a dashboard table tile is configured with an `onClick` action (search
or dashboard link), the row click destination is now visible before the
user clicks. After ~250ms of hover, a small card appears with text like
`Search HyperDX Logs` or `Open dashboard "API Latency Drilldown"`.

The cell wrapper switches from a manual `<div role="link">` with
hand-rolled cmd-click / middle-click / Enter handlers to a Next.js
`<Link href>`. The browser now handles cmd-click and middle-click
opening in a new tab natively, right-click shows "Open in New Tab" /
"Copy Link Address" in the context menu, the destination URL appears in
the status bar on hover, and keyboard users can Tab to a cell and press
Enter to navigate with a visible focus ring.

The cell wrapper now mirrors the pattern in `DBListBarChart.tsx` (the
only other dashboard chart with a row drilldown HoverCard).

A new `describeOnClick` helper in common-utils renders the one-line
hint. The hook returns the description alongside the URL so future hint
surfaces (chart-header subtitle, scope preview) can reuse the same
string.
P0 fix:
- Failure rows now render as <button type="button"> instead of
  <a href="#">. The previous anchor exposed cmd-click, middle-click,
  and right-click "Open in New Tab" against a # fragment, which the
  React onClick handler couldn't preventDefault on (auxclick doesn't
  fire click). A real button has no native navigation, so those code
  paths now do nothing for failure rows and the error toast still
  fires on left-click.

P2 fixes:
- The HoverCard now wraps the whole <tr> in the body loop instead
  of mounting one HoverCard per cell. Hovering anywhere on the row
  shows a stable hint; cursor movement between cells no longer
  flickers a different dropdown per column.
- getRowAction is now computed once per row and the hook caches by
  row reference (WeakMap). The handlebars + URLSearchParams build
  no longer reruns for every cell on every render.
- <Link> now uses prefetch={false}. Virtualized scroll no longer
  triggers an N-row prefetch storm against /search? and /dashboards
  routes the user usually never opens in bulk.
- Added a component-level test (HDXMultiSeriesTableChart.test.tsx)
  that exercises both success and failure branches end to end:
  asserts the anchor / button shape, the onClickError wiring, the
  row-level HoverCard hint visibility, and the legacy
  getRowSearchLink fallback.

Smaller cleanups:
- describeOnClick now does an explicit exhaustiveness check on the
  OnClick discriminator so adding a third variant fails type-check.
- onClickError is typed as React.MouseEvent<HTMLButtonElement>.
- The cell wrapper carries data-testid="dashboard-table-row-action"
  on both branches; e2e page object resolves by testid so future
  inline column links don't steal the click.
- Hook test asserts URL params via URLSearchParams instead of
  position-coupled regex.
…re rows

Addresses three rendering issues Drew flagged on #2321 after
approval:

- Failure rows now share a single .cellButton SCSS reset with the
  success-row <Link>, so the cell wrapper renders identically
  across branches. The user-agent button defaults (padding, font,
  color, text-align, line-height) were leaking through.
- Hover hint is suppressed when action.url === null. The previous
  state showed an "Open in search" hint on a row whose click would
  only fire an error toast, which lied to the user.
- Row-level HoverCard (position="top") replaced with
  Tooltip.Floating wrapping the <tr>. The hint now follows the
  cursor at the cell rather than anchoring at row-center-top.

Tests updated: the failure-row hint test now asserts the hint is
absent; a success-row positive test was added. Both branches get
a data-shape attribute so the test assertions don't couple to the
CSS-module hash.
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Fixed in 19c0204e:

  • Visual parity: shared SCSS reset on the fallback <button> so the wrapper renders identically to the success-row <Link>. The padding / font / color / text-align / line-height that the user-agent button defaults were leaking through are now reset.
  • Hint is suppressed on rows where the templates don't resolve (action.url === null), so the cursor never sees an "Open in search" promise the click won't keep.
  • Row-level HoverCard (position="top") replaced with Tooltip.Floating wrapping the <tr>. The hint now follows the cursor near the cell instead of centering above the row.

One quirk to flag: Mantine v9's Tooltip.Floating doesn't expose an openDelay, so the hint appears instantly instead of after 250ms. Happy to add a small useHover + setTimeout wrapper if you'd prefer the delayed behavior back.

Also re-applied the comment cleanup from dd50d56 (your inline suggestion to drop the internal AC tracking note from the button comment). It was on the branch briefly but the post-approval rebase clobbered it before I could push this fix; folded it into this commit.

ci-lint + ci-unit + knip green locally.

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.

2 participants