Skip to content

feat(databases): tree nav, database overview, and shared table actions#1471

Merged
dawsontoth merged 6 commits into
stagefrom
claude/databases-nav-redesign-1e5b1d
Jul 13, 2026
Merged

feat(databases): tree nav, database overview, and shared table actions#1471
dawsontoth merged 6 commits into
stagefrom
claude/databases-nav-redesign-1e5b1d

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

What & why

Rebuilds the Databases left nav on the same react-complex-tree tech the Applications tab uses, and adds a database overview page. The old dropdown + flat table list only showed one database at a time, had no filter, and kept per-item actions in the right-pane toolbar only.

Now:

  • + Create a Table row at the top → databases as top-level items → tables nested underneath.
  • Type-to-filter (the library's built-in search).
  • Single-click selects — a database opens its new overview page, a table opens its data grid; double-click / chevron expands a database (the Applications model).

Highlights

  • Database overview page — DB size, audit size, table count, and a per-table breakdown (size, columns, primary key, last updated) with record counts backfilled lazily. Sizes/schema are instant from the describe_all map already loaded; counts come from a separate counted describe_all. Opening the tab now lands on the first database's overview.
  • Right-click menus — databases (Create a Table, Drop Database) and tables (Add Record(s), Import Data, Export CSV, Drop Table). The table actions live in one TableContextMenuItems component shared by the tree and the overview's table list, so right-clicking a row in either place opens the same menu and they can't drift.
  • Single modal hub — the action modals were lifted into an always-mounted DatabaseActionModals driven by target-carrying watched values, so an action can target any db/table (even one that isn't open, or an empty database), not just the currently-open one. CSV download extracted into useExportTableCsv (toolbar keeps its filter-aware export; the tree/overview export the whole table).
  • Removed the redundant sidebar Import Data button (it's in the header).

Fixes folded in

  • Context menu didn't re-anchor on a second right-click while already open (Radix keeps it mounted through its exit animation, so it never re-measured) — the menu appeared over the wrong row. Fixed by remounting the content keyed to the cursor position. Also applied to the Applications file tree, which had the same bug.
  • Latent crash in useInstanceSchemaTablePermission (a missing optional chain) that the new right-click menus would trigger for restricted (non-structure_user) users.

No new backend operations

Everything reuses existing operations/modals (create_table, drop_table, drop_database, insert, imports, CSV export). Rename was explicitly out of scope.

Verification

  • Browser-verified end-to-end against a real cluster: tree render, type-to-filter, single/double-click, both context menus (including targeting a not-currently-open table and re-anchoring between rows), the overview stats, and the overview-row menu opening the correctly-targeted modal.
  • Light + dark mode checked; the tree icons now match the Applications palette (green +, orange containers, neutral high-contrast leaves).
  • tsc -b, oxlint, and dprint pass. vitest green (28 tests, incl. new coverage for the tree-item builder).

🤖 Generated with Claude Code

@dawsontoth
dawsontoth requested a review from a team as a code owner July 10, 2026 23:32
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 49.08% 4913 / 10010
🔵 Statements 49.49% 5235 / 10577
🔵 Functions 40.94% 1183 / 2889
🔵 Branches 42.07% 3223 / 7660
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/applications/components/ApplicationsSidebar/ItemArrow.tsx 0% 0% 0% 0% 28-44
src/features/instance/databases/index.tsx 0% 0% 0% 0% 18-66
src/features/instance/databases/components/DatabaseActionModals.tsx 0% 0% 0% 0% 29-110
src/features/instance/databases/components/DatabaseOverview.tsx 0% 0% 0% 0% 17-161
src/features/instance/databases/components/DatabaseTableView.tsx 0% 0% 0% 0% 71-542
src/features/instance/databases/components/DatabasesSidebar.tsx 0% 0% 0% 0% 6-18
src/features/instance/databases/components/TableContextMenuItems.tsx 0% 0% 0% 0% 21-56
src/features/instance/databases/components/TableRowContextMenu.tsx 0% 0% 0% 0% 17-44
src/features/instance/databases/components/DatabasesTree/DatabaseTreeContextMenu.tsx 0% 0% 0% 0% 29-73
src/features/instance/databases/components/DatabasesTree/ItemTitle.tsx 33.33% 0% 0% 33.33% 12-26
src/features/instance/databases/components/DatabasesTree/buildItems.ts 100% 83.33% 100% 100%
src/features/instance/databases/components/DatabasesTree/getItemTitle.ts 80% 75% 100% 80% 13
src/features/instance/databases/components/DatabasesTree/index.tsx 3.22% 0% 0% 3.57% 27-87
src/features/instance/databases/components/DatabasesTree/specialItems.ts 100% 100% 100% 100%
src/features/instance/databases/components/DatabasesTree/useDatabaseTreeViewState.ts 0% 0% 0% 0% 16-47
src/features/instance/databases/functions/resolveDatabasesRedirect.ts 100% 100% 100% 100%
src/features/instance/databases/hooks/useExportTableCsv.ts 0% 0% 0% 0% 23-69
src/features/instance/databases/modals/CreateNewTableModal.tsx 5% 0% 0% 5% 49-165
src/features/instance/databases/modals/DeleteDatabaseModal.tsx 0% 0% 0% 0% 14-62
src/features/instance/databases/modals/DeleteTableModal.tsx 0% 0% 0% 0% 14-67
src/hooks/checkSchemaTablePermission.ts 100% 100% 100% 100%
src/hooks/usePermissions.ts 13.63% 5.76% 15.38% 13.79% 30, 37-43, 51, 58-211
Generated in workflow #1473 for commit 5796f34 by the Vitest Coverage Report Action

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

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.

Code Review

This pull request refactors the database management UI by replacing the select-based sidebar with a tree-based navigation sidebar, introducing a database overview component, and consolidating action modals into a single, always-mounted component. It also extracts CSV export logic into a reusable hook and adds context menus for right-click actions. The review feedback suggests adding unique key props to the consolidated modals to ensure proper state resets when switching targets, and wrapping the CSV blob URL revocation in a setTimeout to prevent download failures in certain browsers.

Comment thread src/features/instance/databases/components/DatabaseActionModals.tsx
Comment thread src/features/instance/databases/hooks/useExportTableCsv.ts Outdated

@DavidCockerill DavidCockerill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Really like this — the tree rebuild matches the Applications model, and showing sizes/schema instantly from the fast describe_all map while backfilling row counts lazily is exactly the right call for the slow part. Overview numbers all check out on read (DB/audit size off any table's db_size/db_audit_size with ?? 0 for empty DBs, table count from Object.keys, PK falls back primary_key ?? hash_attribute ?? '—'), and the empty-instance / stale-link <Navigate replace> fallbacks each terminate cleanly with no redirect loop.

One thing worth confirming before merge — a trade-off, not a bug:

In plain terms: to fill in the row counts, the overview page asks the server to count every table in every database on the whole instance, even though it's only displaying one database — and it fires the moment the Databases tab opens, since that now lands on this overview by default. The server caps the work (it estimates rather than exhaustively scanning once a table gets large, and caches the result ~1 min), so it won't hang the page. But on an instance with many databases/tables it's more server work on tab-open than the old screen, which only counted the one table you were viewing. If that eagerness is intentional (warming the cache for when you expand other DBs), all good — worth a comment saying so. If not, the fix is small: scope the counted describe to the database being shown, or request estimates explicitly. (DatabaseOverview.tsx:43.)

A few smaller, non-blocking notes:

  • usePermissions.ts:214-216 — the attribute-permission function still has the same unguarded specificPermission?.tables?.[tableName] deref you just fixed two lines up in the table-level hook; not reachable through this PR's menus (they use the fixed table-level hook), but cheap to guard while you're here.
  • DatabaseActionModals.tsx:107AddTableRowModal silently renders nothing if its target isn't in the fast map (e.g. a stale map after an external drop); a toast on the miss would be friendlier. Low likelihood.
  • Double-click on a database navigates (first click) then expands (second) — consistent with single-click-opens-overview and the differ-guard makes it idempotent, so it's a non-issue functionally; noting since single/double-click races are worth being explicit about.
  • No tests lock in the lazy count-backfill, the routing fallbacks, or the tree interactions — worth a couple given how much interaction logic moved here.

— DAIvid (Claude Opus 4.8)

@dawsontoth

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough read! Addressed in 20e1ccf7:

  • Whole-instance count on tab-open (DatabaseOverview.tsx:43) — good catch, wasn't a deliberate cache-warm. Switched from one counted describe_all (every table in every DB) to per-table describe_table for only the shown database via useQueries. It uses the same cheap estimate path and now shares React Query's cache with the table view, so opening a table is instant and counts stay consistent.
  • usePermissions.ts:214-216 — guarded the attribute-permission table deref (returns false when the table isn't in the permission map), matching the table-level fix two lines up.
  • DatabaseActionModals.tsx:107 — Add Records now toasts and clears the trigger when the target table is missing from the fast map (stale map after an external drop), instead of silently rendering nothing.
  • Double-click navigate-then-expand — left as-is; the differ-guard makes it idempotent, so it's a no-op functionally. Agreed it's worth being explicit about.
  • Tests — extracted the tab's redirect rules into a pure resolveDatabasesRedirect and covered the fallbacks + loop-freedom (8 cases). The lazy count-backfill and tree-interaction rendering need a query/router harness; I'd suggest those as a focused follow-up rather than growing this PR further.

🤖 Addressed by Claude Code

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed — this is a clean refactor. Rebuilding the Databases nav on react-complex-tree to match Applications, the single always-mounted action-modal hub, and the composite db/table ids all read well, and it's nicely commented and browser-verified end-to-end. No blocking issues, and both Gemini bot findings (per-target key to reset modal state on target switch; deferring URL.revokeObjectURL so the download isn't cancelled on Firefox/iOS Safari) are already applied at head. Two genuine bug fixes folded in are a bonus:

  • the permission crash fix (usePermissions.ts:189) — the missing optional chain that any restricted (non-structure_user) user would trip opening a table context menu; and
  • the CSV export hook now has real error handling (useExportTableCsv.ts:46) where before a failed export left the toast spinning and isExportingCSV stuck true.

The tree builder is the right unit to test and it's tested honestly — buildItems.test.ts covers sort order, the synthetic root, duplicate table names across dbs, empty databases, the canManage gate, and an undefined map. Builder is memoized on [map, canManage], so navigation doesn't rebuild the tree. Good.

Non-blocking items:

Instance-wide counted describe_all on overview open (DatabaseOverview.tsx:33). getDescribeAllQueryOptions without skipRecordCount counts every table in every database, but the overview only shows one database's counts. It's cold-path, cached, and non-blocking (counts backfill with a ), so low severity — but on a large multi-database instance it's a heavier scan than needed. If describe_all supports a per-database count scope, scoping it would avoid touching unrelated databases.

Suggestions: a tiny unit test for the permission crash fix (a permission object where tables[tableName] is undefined) would pin that restricted-user regression; and AddTableRowModal silently no-ops when the target table isn't in instanceDatabaseMap (DatabaseActionModals.tsx:86) — reachable only via a stale tree between invalidation and refetch, so low-risk, but a right-click "Add Record(s)" that does nothing is a confusing dead-end (a toast or refetch-then-open would be friendlier).

One question: does the root "Create a Table" row intentionally seed the modal with the currently-open database rather than no database? Reads deliberate, just confirming.

Perf-wise there's no Harper read/replication hot-path impact here — tree + overview are driven off the single already-loaded describe_all map, no N+1, no re-render storm on nav. Looks good to land.


🤖 Reviewed by KrAIs (Claude Opus 4.8) on Kris's behalf. (Gemini/agy local pass unavailable — synthesis is from the Claude pass + the Gemini bot review already on the PR.)

Comment thread src/features/instance/databases/components/DatabaseOverview.tsx Outdated
Comment thread src/features/instance/databases/modals/DeleteTableModal.tsx Outdated
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Thanks @kriszyp! Follow-ups on the non-blocking items:

  • Instance-wide counted describe_all on overview open — good catch; scoped it in 20e1ccf7 (before this review's head). The overview no longer calls describe_all with counts; it fetches per-table describe_table for only the shown database via useQueries (cheap estimate path), which also shares React Query's cache with the table view. describe_all has no per-database count scope, so per-table was the way to bound it.
  • Permission crash regression test — added in 0de34c9b: extracted the pure checkSchemaTablePermission (no auth-store imports, so it's testable in a node env) and covered the exact case that used to crash — a restricted user whose database grant lacks the table — plus super/structure-user and the per-action flag.
  • AddTableRowModal silent no-op on a stale map — also handled in 20e1ccf7: it now toasts ("…may have just been removed. Try refreshing.") and clears the trigger instead of dead-ending.
  • "Create a Table" root row seeding the open database — yes, intentional. It prefills the modal's Database Name with the currently-open database as a convenience (you're usually working in one); it's editable and falls back to data if cleared. The per-database right-click menu's "Create a Table" seeds that specific database.

All green (tsc / oxlint / dprint / 78 tests). Thanks for the thorough pass!

🤖 Addressed by Claude Code

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed at 283ba95 — thanks for the quick turnaround. All three items from my last pass are correctly resolved: the overview record counts are now scoped to the shown database (and you extracted the redirect logic into a pure, tested resolveDatabasesRedirect along the way — nice), the restricted-user permission crash now has a real regression test (checkSchemaTablePermission.test.ts), and the stale Add-Records target now toasts instead of silently no-op'ing. Approving.

Two small, non-blocking suggestions on the fixes themselves (both low-severity, no rush):

  • Total Records can silently undercount on a settled-but-failed count (DatabaseOverview.tsx:60). allCountsSettled treats "settled" as ready, but countByTable[tableName] is only populated on success, so a failed table contributes 0 via ?? 0 — the total renders as a definite-looking number that's short by that table's count, with no ~/error indicator (anyEstimated only reflects estimated_record_range, not query failure). It's strictly better than the old permanently-stuck , and the trigger is rare (a describe_table failing for a table present in the fast map — a race with a concurrent drop, or a transient 5xx with retry: false), but it trades "obviously stuck" for "quietly wrong," which is the less safe direction for a count. Consider flagging failed-but-settled tables the way estimated ones are flagged (or omitting them from "Tables" for consistency).

  • The attribute-permission crash guard has no unit test (usePermissions.ts:206), unlike its sibling checkSchemaTablePermission. Same one-line-guard pattern, same crash class, and it was the one still-open crash vector from the last pass — worth a parity test, ideally by extracting it the same way you did the sibling.

Neither blocks — approving as-is.


🤖 Reviewed by KrAIs (Claude Opus 4.8) on Kris's behalf. (Gemini/agy unavailable this pass — Claude-only.)

@dawsontoth

Copy link
Copy Markdown
Contributor Author

Spun the deferred review follow-ups out into tracked issues so this PR can land:

The items reviewers flagged that were fixed in-PR (per-database count scoping, permission-crash regression test, stale-map toast, settled-based overview total) remain in this branch.

🤖 via Claude Code

dawsontoth and others added 6 commits July 13, 2026 15:27
Rebuild the Databases left nav on the applications tab's react-complex-tree:
databases as top-level items with their tables nested, a "+ Create a Table"
row, and built-in type-to-filter. Single-click selects (database -> new
overview page, table -> data grid); double-click / chevron expands.

- Add a database overview page: DB size, audit size, table count, and a
  per-table breakdown (size, columns, primary key, last updated) with record
  counts backfilled lazily. Landing on the tab now shows the first DB's overview.
- Right-click menus for databases (Create a Table, Drop Database) and tables
  (Add Record(s), Import Data, Export CSV, Drop Table). The table actions live
  in one TableContextMenuItems component shared by the tree and the overview's
  table list, so the two menus can't drift.
- Lift the action modals into a single always-mounted DatabaseActionModals hub
  driven by target-carrying watched values, so an action can target any
  db/table -- not just the open one. Extract the CSV download into useExportTableCsv.
- Fix a Radix quirk where an already-open context menu didn't re-anchor on a
  second right-click: remount the content keyed by cursor position. Also applied
  to the applications file tree, which had the same bug.
- Fix a latent crash in useInstanceSchemaTablePermission (missing optional chain)
  that the new right-click menus would trigger for restricted users.
- Remove the redundant sidebar "Import Data" button (already in the header).

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

Address PR review:
- Give the consolidated action modals a per-target `key` so their internal
  state resets on switch, and drop CreateNewTableModal's manual reset effect
  (the remount now seeds the prefilled database).
- Defer URL.revokeObjectURL in useExportTableCsv so it can't cancel the
  download before Firefox / iOS Safari start fetching the blob.

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

Address PR review:
- Overview row counts now fetch per-table describe_table for ONLY the shown
  database (via useQueries) instead of a whole-instance counted describe_all on
  tab-open; shares cache with the table view. (DatabaseOverview.tsx)
- Guard the attribute-permission table deref in useInstanceSchemaTableAttribute-
  Permission, matching the table-level fix. (usePermissions.ts)
- Toast + clear the trigger when Add Records targets a table missing from the
  fast map (stale map after an external drop). (DatabaseActionModals.tsx)
- Extract the tab's redirect rules into resolveDatabasesRedirect (pure, loop-free)
  and cover them with unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the pure table-permission check into checkSchemaTablePermission (no
auth-store imports, so it's unit-testable in a node env) and cover the case that
previously crashed: a restricted (non-structure_user) user whose database grant
doesn't list the table. useInstanceSchemaTablePermission now delegates to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review:
- DatabaseOverview: base the aggregate Records total on settled count queries
  (not data presence) so a single failed/never-resolving describe_table (retry:
  false) no longer hangs it on "…"; prefix the total with "~" when any
  contributing count is estimated, matching the per-row labels.
- DeleteTableModal / DeleteDatabaseModal: drop the now-always-undefined `trigger`
  + attemptToRestoreFocus plumbing (callers fire via a target payload with no
  trigger). Radix's confirmation dialog restores focus on close on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lifted AddTableRowModal takes stage's optional databaseTables prop so record
add is relationship-aware, matching what the right-pane toolbar passes. (This
integration lived in the dropped merge commit; restoring it after the rebase.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/databases-nav-redesign-1e5b1d branch from 283ba95 to 5796f34 Compare July 13, 2026 19:31
@dawsontoth
dawsontoth added this pull request to the merge queue Jul 13, 2026
Merged via the queue into stage with commit f1c0ae6 Jul 13, 2026
2 checks passed
@dawsontoth
dawsontoth deleted the claude/databases-nav-redesign-1e5b1d branch July 13, 2026 19:52
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.

4 participants