Skip to content

refactor(app): split DBDashboardPage into focused modules#2233

Open
fleon wants to merge 14 commits intomainfrom
fleon/refactor-DBDashboardPage
Open

refactor(app): split DBDashboardPage into focused modules#2233
fleon wants to merge 14 commits intomainfrom
fleon/refactor-DBDashboardPage

Conversation

@fleon
Copy link
Copy Markdown
Contributor

@fleon fleon commented May 7, 2026

Summary

Refactors the large DBDashboardPage module into a DBDashboardPage/ directory with focused components for the tile renderer, heatmap tile, edit modal, container row, header, toolbar, and dashboard grid. The public @/DBDashboardPage import remains unchanged through a barrel export.

Moves pure dashboard layout/grouping helpers into utils.ts and adds targeted tests for helper behavior and dashboard grid actions. This is intended to make future dashboard changes easier to review without changing runtime behavior.

How to test locally or on Vercel

  1. cd packages/app && yarn ci:unit src/DBDashboardPage/__tests__/utils.test.ts src/DBDashboardPage/__tests__/DashboardGrid.test.tsx
  2. cd packages/app && yarn ci:unit
  3. make ci-lint currently reaches unrelated pre-existing source.disabled type errors in packages/api/src/models/source.ts and source UI files; the dashboard refactor files have no IDE linter diagnostics after yarn lint:fix.
  4. make dev-e2e FILE=dashboard was started locally; it was still running after the command was manually backgrounded while the E2E stack was pulling ClickHouse.

References

  • Linear Issue: N/A
  • Related PRs: N/A

Made with Cursor

fleon and others added 13 commits May 7, 2026 15:46
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 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 7, 2026 3:43pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

⚠️ No Changeset found

Latest commit: 3064f83

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label May 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Large diff: 5286 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 12
  • Production lines changed: 5286 (+ 259 in test files, excluded from tier calculation)
  • Branch: fleon/refactor-DBDashboardPage
  • Author: fleon

To override this classification, remove the review/tier-4 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 7, 2026

PR Review

Pure refactor splitting the 2464-line DBDashboardPage.tsx into focused modules behind a barrel export. Public @/DBDashboardPage import path is preserved (verified against pages/dashboards/index.tsx and [dashboardId].tsx). New helper-utility and DashboardGrid tests are useful additions.

Findings

  • ⚠️ DashboardContainerRow.tsx:64-70visibleTiles (and the useMemo for layoutChangeHandler derived from it) is computed twice: once at the outer scope from the activeTabId prop, and again inside the children render-prop from currentTabId. RGL renders the inner set but uses the outer-built handler. This works only because both paths fall back to tabs[0]?.id consistently — if DashboardContainer's internal resolvedActiveTabId ever diverges from the prop, the warning the comment cites ("bogus diff + setDashboard write") returns. → Build the layout handler from the inner visibleTiles (inside the render-prop) so they can never disagree, or drop the duplication entirely.
  • ⚠️ Same spot — containerTiles.filter(...) returns a new array every render, so the useMemo dep [makeLayoutChangeHandler, visibleTiles] is unstable and the memo is effectively a no-op. Either memoize visibleTiles or remove the useMemo.
  • ℹ️ File size: DBDashboardPage/DBDashboardPage.tsx is 955 lines and DashboardTile.tsx is 712 — both above the CLAUDE.md "<300 lines" guideline. Big improvement from 2464, but the orchestrator and tile components are obvious next targets (e.g. extract the renderTileComponent chart-by-displayType switch, the URL-collapse-state hook, and the saved-query handlers).
  • ℹ️ DBDashboardPage.tsx:445-555renderTileComponent is a useCallback that closes over ~14 dependencies; any one changing rebuilds it and re-renders every tile. Not a regression (same behavior as before the split), but worth flagging as a follow-up if perf matters here.

No security, type-safety, or behavioral regressions spotted. Tests look reasonable (the setupTests.tsx global renderWithMantine is correctly available to the new test file).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

E2E Test Results

All tests passed • 166 passed • 3 skipped • 1180s

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

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Deep Review

Refactor of the 2464-line packages/app/src/DBDashboardPage.tsx into a DBDashboardPage/ directory of focused components plus shared utils.ts/types.ts. Mostly a faithful extraction; behavior should be preserved. The diff also shows packages/api/src/utils/redactSecrets* deletions because the branch hasn't been rebased onto main — those are not part of this PR's intent and were not reviewed.

🔴 P0/P1 -- must fix

  • packages/app/src/DBDashboardPage/DashboardTile.tsx:70 -- the refactor dropped the import of displayTypeRequiresSource from @hyperdx/common-utils/dist/guards and inlined a local copy that only checks DisplayType.Markdown, so any future sourceless DisplayType added upstream silently won't be honored here.
    • Fix: Delete the local const and import displayTypeRequiresSource from @hyperdx/common-utils/dist/guards, matching what the original DBDashboardPage.tsx did.
    • correctness, maintainability, adversarial, kieran-typescript
  • packages/app/src/DBDashboardPage/DashboardTile.tsx -- the largest extracted component (712 lines: queriedConfig derivation, alert-indicator color logic across 4 alert states, source-missing/unset states, filter-warning macro detection, fullscreen f-hotkey toggle, shift-click selection, the 7-branch renderChartContent DisplayType dispatch) has zero new tests, and the refactor is the natural moment to lock that surface in.
    • Fix: Add tests covering at minimum isSourceMissing / isSourceUnset messaging, alertIndicatorColor for OK/ALERT/silenced states, filterWarning rendering when SQL templates lack $__filters, and the f-hotkey fullscreen toggle.
  • packages/app/src/DBDashboardPage/utils.ts:25 -- updateLayout, downloadObjectAsJson, and getDashboardTableConnections are exported and used in critical paths (layout save, dashboard export, filter autocomplete) but have no tests, while their sibling helpers in the new __tests__/utils.test.ts do.
    • Fix: Add unit tests for each: updateLayout mutating tile coords by id, downloadObjectAsJson with mocked DOM helpers, and getDashboardTableConnections covering each early-return (missing dashboard, non-builder config, missing source, missing tableName).

🟡 P2 -- recommended

  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx -- still 955 lines after the split, exceeding the project's 300-line ceiling per AGENTS.md Key Principles feat: add docker prod build stages and publish prod builds #4 and agent_docs/code_style.md; the directional improvement is large but the file-level standard is not yet met.
    • Fix: Extract a useDashboardContainerState hook for the URL-collapse/expand/active-tab management (lines ~344–660) and a useDashboardInitialization hook for the saved-query/saved-filter init effect (~210–265).
    • project-standards, maintainability
  • packages/app/src/DBDashboardPage/DashboardTile.tsx -- 712 lines after the split, also exceeding the 300-line ceiling; the renderChartContent callback alone is a ~180-line DisplayType dispatcher that could stand on its own.
    • Fix: Extract renderChartContent into a TileChartContent component and split the hover-toolbar JSX into a TileHoverToolbar; re-evaluate file size after.
    • project-standards, maintainability
  • packages/app/src/DBDashboardPage/DashboardContainerRow.tsx:64 -- visibleTiles is computed twice: once on the outer scope (from prop activeTabId, used to bind makeLayoutChangeHandler) and again inside DashboardContainer's render-prop (from currentTabId). When the two ids diverge — e.g., transient activeTabId=undefined while currentTabId falls back to tabs[0].id — the layout handler diffs the wrong tile set and writes bogus layouts.
    • Fix: Move visibleTiles derivation inside the render-prop and build the layout handler from the same currentTabId-derived set; rename the outer variable so the shadowing is impossible.
    • maintainability, adversarial, correctness
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:557 -- makeOnLayoutChange closes over dashboard and gridTiles, so a tile delete that lands while a drag/resize is mid-flight can resurrect the deleted tile when RGL flushes its final onLayoutChange against the pre-delete snapshot (pre-existing, carried verbatim).
    • Fix: Hold the live dashboard in a useRef updated by a useEffect, read dashboardRef.current inside the handler, and filter newLayout against currently-rendered tile ids before producing.
    • adversarial, julik-frontend-races
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:464 -- onDuplicateClick and onDeleteClick await confirm(...), then spread the closure-captured dashboard into setDashboard; concurrent layout/refresh writes that land between dialog open and confirm get clobbered (pre-existing).
    • Fix: Read dashboardRef.current (same ref pattern as the layout-handler fix) inside the confirm continuation instead of the closure variable.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:30 -- useDashboard is imported on its own line right after the named-import block on lines 24–29 that already pulls from @/dashboard.
    • Fix: Merge useDashboard into the existing block and delete line 30.
  • packages/app/src/DBDashboardPage/DashboardGrid.tsx:48 -- onAddTile is declared (containerId?: string, tabId?: string) => void here but (containerId: string, tabId?: string) => void in DashboardContainerRow.tsx:24; the same callable is typed two different ways.
    • Fix: Align both signatures to (containerId?: string, tabId?: string) => void so the contract is single-sourced.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:455 -- renderTileComponent builds a fresh filters array literal ([{type, condition}, ...filterQueries]) on every render and depends on the entire dashboard object, so any tile mutation invalidates the callback and re-runs every DashboardTile's queriedConfig useEffect for all N tiles (pre-existing in shape, but the split makes the re-render fan-out more expensive).
    • Fix: Hoist the filters array into its own useMemo keyed on [where, whereLanguage, filterQueries], and replace the dashboard dependency with a ref-read for the per-tile mutation callbacks.
  • packages/app/src/DBDashboardPage/DashboardTile.tsx:101 -- DashboardTile, DashboardGrid, and DashboardContainerRow are not wrapped in React.memo, so every parent state change cascades through the full tree (pre-existing, but the refactor's prop-drilling makes memoization more impactful once props are stabilized).
    • Fix: After stabilizing the upstream props, wrap each as memo(forwardRef(...)) / memo(...); do this together with the filters-memo and dashboard-ref fixes so the comparison actually short-circuits.
  • packages/app/src/DBDashboardPage/utils.ts:39 -- hasLayoutChanged and updateLayout use Array.find inside a per-item loop, so both are O(N²) for every drag event; with 50+ tiles this is measurable on each pointer move (pre-existing).
    • Fix: Build const layoutMap = new Map(currentLayout.map(l => [l.i, l])) (or newLayout for updateLayout) once outside the loop and look up via Map.get.
  • packages/app/src/DBDashboardPage/utils.ts:89 -- getDashboardTableConnections calls sources?.find(v => v.id === config.source) inside the per-tile loop (O(tiles × sources)) (pre-existing).
    • Fix: Build const sourceMap = new Map(sources.map(s => [s.id, s])) before the loop and look up via Map.get(config.source).
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:260 -- the dashboard-initialization useEffect lists router.query (the whole object, which gets a fresh reference on every nuqs URL update) in its deps, risking spurious re-runs on unrelated query changes (pre-existing).
    • Fix: Hoist const hasWhereInUrl = 'where' in router.query and const hasFiltersInUrl = 'filters' in router.query outside the effect and depend on those booleans instead.
  • packages/app/src/DBDashboardPage/HeatmapTile.tsx:34 -- toHeatmapChartConfig(queriedConfig) runs unconditionally at the top of the component, so popover hover/click state changes redo the config derivation (pre-existing).
    • Fix: Wrap in useMemo(() => toHeatmapChartConfig(queriedConfig), [queriedConfig]).
  • packages/app/src/DBDashboardPage/DashboardGrid.tsx:161 -- DashboardGrid builds inline arrow handlers (onToggleCollapse, onTabChange, onRenameTab, etc.) per container per render, adding a new layer of inline closures that the original single-file component didn't have.
    • Fix: Pass the parent-level handleToggleCollapse/handleTabChange handlers through unchanged, or push the per-container binding into a memoized child component keyed on container.id.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:50 -- DashboardQueryFormValues and MoveTarget are imported as values but used only in type positions (useForm<…>, useMemo<…[]>).
    • Fix: Change to import type { DashboardQueryFormValues, MoveTarget } from './types';.
  • packages/app/src/DBDashboardPage/__tests__/DashboardGrid.test.tsx:16 -- the new test only exercises canRenderDashboard=false; the actual grid path with containers + ungrouped tiles + renderTileComponent invocations is never rendered.
    • Fix: Add a test with canRenderDashboard=true, at least one ungrouped tile, and one container, asserting renderTileComponent was invoked per tile and DashboardContainerRow received the right props.
  • packages/app/src/DBDashboardPage/EditTileModal.tsx:37 -- handleClose has three branches (isSaving=true blocks close, hasUnsavedChanges=true shows confirm, otherwise close immediately) and none are tested.
    • Fix: Add tests for all three branches, including the case where the confirm dialog is cancelled and onClose must not fire.
  • packages/app/src/DBDashboardPage/HeatmapTile.tsx:53 -- click-to-popover, dismiss-on-overlay-click, and the eventDeltasUrl null-source guard have no tests.
    • Fix: Add tests asserting the popover opens only when source is defined, the "View in Event Deltas" link renders when eventDeltasUrl resolves, and clicking the backdrop resets clickPos.
  • packages/app/src/DBDashboardPage/DashboardContainerRow.tsx:59 -- hasTabs true/false branching for visibleTiles and the makeLayoutChangeHandler binding is uncovered.
    • Fix: Add tests for hasTabs=true (only activeTabId tiles flow into the handler), hasTabs=false (all containerTiles do), and behavior when activeTabId changes.
  • packages/app/src/DBDashboardPage/__tests__/utils.test.ts:51 -- the newLayout.length !== currentLayout.length early return in hasLayoutChanged is not covered.
    • Fix: Add a case with mismatched lengths asserting true.
  • packages/app/src/DBDashboardPage/__tests__/utils.test.ts:117 -- getUngroupedTiles with hasContainers=false (returns allTiles unfiltered) is uncovered.
    • Fix: Add a case with hasContainers=false and a mix of with/without containerId asserting all tiles are returned.
  • packages/app/src/DBDashboardPage/__tests__/utils.test.ts:142 -- getAlertingTabIdsByContainer with a no-tabs container plus an alerting tile that has no tabId (silently dropped) is uncovered, as is the empty-alerting-set skip path.
    • Fix: Add cases for both shapes asserting the container is absent from the result map.
🔵 P3 nitpicks (10)
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:161 -- whereLanguage form defaultValues now narrows to 'sql'|'lucene' and falls back to stored language for any other value, where the original cast (whereLanguage as SearchConditionLanguage) passed any URL string through; the downstream sync useEffect already validates, so this is a tiny first-render divergence.
    • Fix: Confirm the narrowing is intentional (it likely is — matches the sync effect) and leave a comment, or revert to the cast for first-render parity.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:592 -- stale urlCollapsedIds/urlExpandedIds accumulate forever for deleted containers; toggling a stale id silently re-adds it.
    • Fix: Prune both arrays against the current dashboard.containers whenever containers change, or strip the deleted id inside handleDeleteContainer.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:681 -- rapid Add Tile clicks before the previous save propagates compute identical (x, y) from the stale dashboard.tiles, so two new tiles overlap.
    • Fix: Disable Add Tile while isSaving is true, or include in-flight tiles in calculateNextTilePosition.
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:121 -- useRef<string>(undefined) declares .current as string but initializes to undefined.
    • Fix: Change to useRef<string | undefined>(undefined).
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:101 -- useQueryState('granularity', parseAsString) as [SQLInterval | undefined, …] silently coerces any URL string to SQLInterval, including 'auto' if it ever leaks.
    • Fix: Build the nuqs parser flagged by the existing // TODO: Build parser comment, or guard setGranularity with if (watchedGranularity !== 'auto').
  • packages/app/src/DBDashboardPage/DBDashboardPage.tsx:952 -- @ts-expect-error for getLayout papers over a real type gap on the dynamic component (pre-existing).
    • Fix: Type DBDashboardPageDynamic as the dynamic component intersected with { getLayout: typeof withAppNav } so the assignment type-checks.
  • packages/app/src/DBDashboardPage/DashboardTile.tsx:79 -- onAddAlertClick and onSettled are declared in DashboardTileProps but never destructured or used (pre-existing).
    • Fix: Confirm with the author whether onAddAlertClick was supposed to wire the alert bell (currently calls onEditClick), then either wire it or remove both props.
  • packages/app/src/DBDashboardPage/__tests__/DashboardGrid.test.tsx:66 -- onGroupSelected and onAddContainer use toHaveBeenCalled() rather than toHaveBeenCalledTimes(1), allowing spurious extra calls to pass.
    • Fix: Switch both assertions to toHaveBeenCalledTimes(1).
  • packages/app/src/DBDashboardPage/__tests__/DashboardGrid.test.tsx:57 -- the singular "1 tile selected" branch (when selectedTileIds.size === 1) is not asserted.
    • Fix: Add a one-id case asserting getByText('1 tile selected') (no trailing 's').
  • packages/app/src/DBDashboardPage/DashboardHeader.tsx:64 -- isLocalDashboard=true (temporary banner + "Create New Saved Dashboard" button) and the saved-defaults menu items are not tested.
    • Fix: Add render tests for both isLocalDashboard branches and for hasSavedQueryAndFilterDefaults toggling the relevant menu items.

Reviewers (10): correctness, testing, maintainability, project-standards, adversarial, performance, kieran-typescript, julik-frontend-races, agent-native (no findings — pure refactor with no new agent-facing surface), learnings-researcher (no findings — docs/solutions/ does not exist in this repo yet).

Testing gaps:

  • DBDashboardPage.tsx (955 lines) has no tests; the saved-query/saved-filter initialization effect, URL collapse-state precedence, and handleMoveTileToGroup position recalculation are all untested.
  • DashboardToolbar.tsx, DashboardHeader.tsx, and DashboardContainerRow.tsx are new files without dedicated tests.
  • No test exercises the tile-delete + RGL onLayoutChange race or the confirm-dialog-then-setDashboard stale-closure path.

@teeohhem
Copy link
Copy Markdown
Contributor

teeohhem commented May 7, 2026

/just-fix-it

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

Fixed:

  • DashboardTile.tsx:70 — restored displayTypeRequiresSource import from @hyperdx/common-utils/dist/guards, deleted the local stub.
  • DashboardTile tests — new __tests__/DashboardTile.test.tsx covers source-missing/unset (incl. Markdown), alertIndicatorColor for OK/ALERT/silenced/no-alert, filter-warning rendering, and fullscreen toggle (button click + f-no-hover negative).
  • utils tests — added cases for updateLayout mutation, downloadObjectAsJson (mocked DOM helpers), and getDashboardTableConnections (each early return + happy path).
  • DBDashboardPage.tsx:30 — merged duplicate useDashboard import into the existing @/dashboard block.
  • DBDashboardPage.tsx:50 — converted DashboardQueryFormValues/MoveTarget to type-only imports.
  • DashboardContainerRow.tsx:24 — aligned onAddTile signature to (containerId?: string, tabId?: string) => void.

Skipped:

  • P1 f-hotkey positive case — useHotkeys + React 19 useEffectEvent didn't pick up state updates inside synchronous test sequences; covered the same setIsFullscreen surface via the toolbar button click + the no-hover negative.
  • All other P2 findings (visibleTiles shadowing, ref-pattern races, useMemo extractions, React.memo wrapping, O(N²) Map refactors, hook extractions, RGL race fixes) — non-mechanical per rule 2.
  • All P3 nitpicks per rule 3.

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

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants