Skip to content

fix(web): UI papercuts — create shortcuts, palette ranking, data browser, trace legend, deep-link Back - #563

Merged
dviejokfs merged 6 commits into
mainfrom
fix/ui-common-fixes
Aug 6, 2026
Merged

fix(web): UI papercuts — create shortcuts, palette ranking, data browser, trace legend, deep-link Back#563
dviejokfs merged 6 commits into
mainfrom
fix/ui-common-fixes

Conversation

@dviejokfs

@dviejokfs dviejokfs commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

A pass over recurring UI papercuts, driven by dogfooding. Frontend only — 24 files, all under web/src/, no backend or migration changes.

1. Sole create actions get an N shortcut

CreateActionButton (badge + shortcut) already existed but only 14 places used it. Ten list pages had a create button as their only primary action and no shortcut: Teams, Dashboards, Metric alerts, S3 sources, Notification providers, Routes, Monitors, Feature flags, Error alert rules, Email domains.

Where a page shows the button in both the header and the empty state, only the header registers the shortcut so one keypress can't fire twice. Where the header button is hidden while the list is empty (notification providers, email domains) the two branches are mutually exclusive and the empty-state button carries it.

Bug found in the shared hook: useKeyboardShortcut fired whenever focus wasn't in a text field, including with a modal open — pressing N with the S3 edit dialog up navigated the page out from under it. It now bails while a Radix dialog, alert dialog, menu or select is open. That fix applies to all 14 pre-existing usages too.

2. Command palette ranks by relevance

Results were bucketed by section and the sections rendered in a fixed order, so a weak keyword hit in Navigation outranked an exact title match in Settings — searching work put Sandboxes above Worker Nodes. Now one list ordered by score, with the section demoted to a right-aligned label.

Fuse alone wasn't enough: a zero-distance keyword hit beats a fuzzy title hit at any per-key weight, which put Users (tagged team) above Teams. Title matches now score explicitly (titleBoost, 0–0.6) and the Fuse component is capped at 0.25, so tags still rank but never above a title match.

Also: Teams and Create Team were missing from the palette entirely, and project sub-pages are now reachable from anywhere (demo deploy → that project's Deployments) for a bounded set of pages per project.

3. Data browser

  • The tree/table split is draggable on desktop, width persisted per service. Mobile keeps the overlay drawer, where a resize handle has nothing to drag against.
  • The mobile tree toggle had no accessible name.

The sort fix originally in this PR was reverted. Rebasing onto main brought in the data-browser redesign (#557), which had already fixed the stale-sort symptom independently via effectiveSortField (validates the sort column against the entity's own fields). Layering my navigateTo routing on top of that collapsed the tree on entity selection — confirmed by running the same switch-tables flow against unmodified origin/main (passes) and against this branch with the change (fails). Reverted rather than ship a fix plus a new regression. Residual gap: the stale filter still carries across a table switch; that needs a fix derived from the redesigned navigation and is left as a follow-up.

Note: react-resizable-panels v4 renames PanelGroup/PanelResizeHandle to Group/Separator, so the published shadcn resizable snippet does not compile against the version already in package.json. The wrapper here targets v4.

4. Unified trace: project legend instead of a slug per span

The per-span project badge truncated to galachain-gat… on every row while still costing ~88px of the name column. Rows now carry only the colour dot; a legend above the waterfall decodes the colours with the full slug, and the name stays reachable via tooltip/aria-label.

CrossProjectTraceDetail already had a legend. TraceDetail's inline unified view (?view=unified) did not — it tagged every span but never said what the colours meant — so that view gets one too.

5. Back works on deep links

navigate(-1) silently does nothing when a page is opened directly (shared URL, new tab, bookmark) — there's no earlier entry to pop, so the user clicks Back and stays put with no feedback. Trace detail is exactly the kind of page people paste to each other.

New useGoBack(fallback) uses react-router's history.state.idx to detect "nothing of ours behind us" and navigates to the parent instead. Applied to all 19 call sites across 8 files, each with its real parent. The forms' post-save navigate(-1) had the same bug — save on a deep-linked form and you were stranded on it.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Evidence

Run against a real local instance (slot 15) with a provisioned Postgres service and two seeded tables with deliberately disjoint columns. 25 Playwright checks, all passing. The specs were written to verify these changes and deleted afterwards — they are not part of this PR.

Shortcuts (15 checks) — badge renders as N; n opens the dialog (Teams, Monitors, Feature flags) or navigates (Backups, Routes, Notifications, Dashboards, Metric alerts, Alert rules); typing n into a field does not fire; n with a dialog open does not navigate.

Palette (7 checks)

✓ "work" ranks Worker Nodes above Sandboxes
✓ "team": Teams first, keyword-only Users still listed but below
✓ "domains": exact title beats every project sub-page
✓ Teams is reachable from the palette
✓ Create Team from the palette opens the dialog
✓ project detail pages are reachable from anywhere
✓ N shortcut still opens the create dialog (after URL-owned dialog refactor)

Data browser (3 checks)

✓ switching tables drops the previous table sort column
✓ sidebar is resizable on desktop      (tree 320px -> 500px after drag)
✓ mobile keeps the drawer and shows no resize handle

Trace + Back (7 checks) — legend renders untruncated slugs, dot count equals span count, dots carry no text; and:

✓ deep-linked trace detail: Back goes to the traces list   (asserts history.state.idx === 0 first)
✓ deep-linked global trace: Back goes to a contributing project
✓ deep-linked IP detail: Back goes to proxy logs
✓ in-app navigation still pops history rather than using the fallback

That last one is the regression guard: it proves history navigation wasn't just replaced with a hardcoded redirect.

Static: tsc --noEmit clean. eslint clean on every changed file — the two remaining errors in ServiceDataBrowser.tsx and one in AlertRuleForm.tsx are pre-existing and were confirmed against the origin/main baseline.

Not verified at runtime

  • Email → Domains N shortcut. Its create button only appears once an email provider is configured, which needs real SMTP credentials. Same code shape as the notification-providers case that does pass, and it typechecks.
  • Unified trace rendering used a mocked /api/otel/global/traces/:id response rather than real ingest (that needs ClickHouse and cross-project OTel spans sharing a trace_id). This drives the real components with realistic data shaped by the generated UnifiedTrace type, but does not prove the backend returns that shape.

Checklist

  • I have written tests that cover the changes — as throwaway Playwright specs, see Evidence; not committed
  • All new and existing tests pass (cargo test --lib) — n/a, no Rust changes
  • cargo check --lib passes with no warnings — n/a, no Rust changes
  • My commits follow the Conventional Commits format
  • I have updated documentation where necessary — no user-facing docs cover these surfaces

Related issues

None.

…overlays

Every list page whose only primary action is 'create' now uses
CreateActionButton, so the button renders a visible N badge and the
shortcut actually works: Teams, Dashboards, Metric alerts, S3 sources,
Notification providers, Routes, Monitors, Feature flags, Error alert
rules and Email domains.

Where a page shows the create button in both the header and the empty
state, only the header registers the shortcut so a single keypress can't
fire twice; where the header button is hidden while the list is empty
(notification providers, email domains) the two are mutually exclusive
and the empty-state button carries it instead.

useKeyboardShortcut also now bails while a Radix dialog, alert dialog,
menu or select is open. Previously a bare N pressed with an edit dialog
up would navigate the page out from under it.
…a-browser tree

Command palette:
- Results are one list ordered by score instead of fixed sections rendered
  in a fixed order. A weak keyword hit in Navigation used to outrank an exact
  title match in Settings purely because Navigation renders first ('work' put
  Sandboxes above Worker Nodes).
- Title matches now score explicitly on top of the Fuse score. Fuse ranks a
  zero-distance *keyword* hit above a fuzzy *title* hit at any per-key weight,
  which put Users (tagged 'team') above Teams. Tags still rank, capped well
  below any title match.
- Teams and Create Team were missing entirely; the Teams dialog is now opened
  by ?new=1 so the palette can deep-link it.
- Project sub-pages are reachable from anywhere ('demo deploy' -> that
  project's Deployments), for a bounded set of pages per project.

Data browser:
- Selecting a different table kept the previous table's sort column and
  filter, so the query sorted on a field the new table has no column for.
  Tree selection went through a bare setSearchParams instead of navigateTo,
  which is what resets sort/filter/page for a new target.
- The tree/table split is draggable on desktop via a shadcn resizable wrapper
  (react-resizable-panels v4 renames PanelGroup/PanelResizeHandle to
  Group/Separator, so the published shadcn snippet does not compile). Width
  persists per service. Mobile keeps the overlay drawer, where a resize
  handle has nothing to drag against.
- The mobile tree toggle had no accessible name.
…per span

The per-span project badge truncated to ~"galachain-gat…" on every row while
still costing ~88px of the name column, which on narrow viewports is most of
the space the span name has. Each row now carries only the colour dot, and a
legend above the waterfall decodes the colours with the full slug; the name
stays reachable via the dot's tooltip and aria-label.

CrossProjectTraceDetail already had a legend. TraceDetail's inline unified
view (?view=unified) did not — it tagged every span but never said what the
colours meant, so that view gets one too. Legend entries drop the badge's
88px cap, since truncating the thing that decodes the dots defeats it.
navigate(-1) silently does nothing when a page was opened directly — a
shared URL, a new tab, a bookmark — because there is no earlier entry in
the history session to pop. The user clicks Back and simply stays put, with
no feedback. This is the common case for trace detail, which is exactly the
kind of page people paste to each other.

Adds useGoBack(fallback): react-router records its position in the session
as window.history.state.idx, so when that is 0 (or null, on the first entry)
there is nothing of ours behind us and we navigate to the fallback instead —
which is where the button's label claims it goes anyway. In-app navigation is
unchanged and still pops history.

Applied to all 19 call sites across 8 files, each with the parent it belongs
to: trace detail -> the project's trace list, the global unified trace -> a
contributing project's trace list, alert/dashboard/metric-alert forms -> their
lists, cron job -> the project's cron jobs, funnel -> the project's funnels,
IP geolocation -> proxy logs. The form ones also covered the post-save
navigate(-1), which stranded the user on a saved form.
Rebasing onto main brought in the data-browser redesign (#557), which had
already fixed the stale-sort symptom independently: effectiveSortField
validates the sort column against the entity's own field names and falls
back to unsorted when it doesn't match.

Routing the three tree-selection handlers through navigateTo on top of that
collapsed the tree on entity selection — verified by running the same
switch-tables flow against unmodified origin/main (passes) and against this
branch (fails, 'public' collapses so sibling tables are unreachable). Since
the reported bug is already fixed upstream, reverting is strictly better
than shipping a working fix plus a new regression.

The residual gap is the stale *filter*, which still carries across a table
switch; that needs a fix derived from the redesigned navigation and is left
as a follow-up rather than guessed at here.
@dviejokfs
dviejokfs force-pushed the fix/ui-common-fixes branch from b350719 to afa8457 Compare August 6, 2026 11:52
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

📓 Changelog preview

This is what your commits will add to the generated CHANGELOG.md at release time (via git-cliff). Do not edit CHANGELOG.md by hand — it is generated from your Conventional Commit messages.

## [Unreleased]

### Fixed

- **web:** Give sole create actions an N shortcut and guard it behind overlays
- **web:** Rank palette results by relevance, add Teams, resizable data-browser tree
- **web:** Show unified-trace projects as a colour legend, not a slug per span
- **web:** Make Back work on deep-linked pages
- **web:** Address self-review findings on the shortcut hook and project dot

### Revert

- **web:** Drop the data-browser tree navigateTo routing

…ct dot

- useKeyboardShortcut re-registered its keydown listener on every render:
  callers pass an inline arrow, so `callback` was a new identity each time
  and the effect tore the listener down and re-added it. Held in a ref
  instead. Affects all 24 call sites, including the 14 that predate this
  branch.
- ProjectDot carried an aria-label on a role-less span, which most screen
  readers ignore — with the slug text now gone that left the project
  unreadable. Added role="img".
- resizable.tsx used the React UMD global for a type; import it explicitly.
@dviejokfs
dviejokfs merged commit 48f497c into main Aug 6, 2026
22 checks passed
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.

1 participant