feat(dashboards): smart views sidebar with tag-only rules (chained on #2379)#2382
Conversation
…iscriminator) Adds the `SmartView*` schemas to common-utils ahead of the model, router, and UI work. The rule discriminated union is intentionally narrow in v1 (`tag-includes`, `tag-excludes`, `untagged`) so the storage + sidebar plumbing can ship without dragging in non-tag rule machinery; a follow-up widens the union with recency / has-alerts / created-by-me / provisioned / has-tile-type kinds and existing documents keep parsing because the extension is additive. The `resource` discriminator already includes `savedSearch` so the Saved Searches sidebar parity work drops in without a schema change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mongoose model mirrors `favorite.ts` for the per-user index pattern
and `dashboard.ts` for the Mixed-typed JSON column that stores the
rules array. The Zod schema in `@hyperdx/common-utils/dist/types`
is the source of truth for rule shape; widening the rule union
later does not require a model migration.
Controller exposes `getSmartViews(userId, teamId, resource?)`,
`getSmartView`, `createSmartView`, `updateSmartView`,
`deleteSmartView`, all scoped by `{ owner, team }`. Cross-user and
cross-team access fall through to a 404 in the router.
Router mounted at `/smart-views` next to `favoritesRouter` and
`savedSearchRouter`. Body and query validation via
zod-express-middleware against the Zod schemas. Tests round-trip
POST -> GET, exercise the resource discriminator filter, and confirm
that another user on the same team cannot patch or delete the
view (both 404).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`packages/app/src/smartView.ts` exposes `useSmartViews(resource)`,
`useCreateSmartView`, `useUpdateSmartView`, `useDeleteSmartView`.
React Query keys are `['smart-views', resource]` so a mutation that
changes one resource's views does not invalidate the other's cache.
`packages/app/src/utils/evaluateSmartView.ts` is a pure function
that takes `{ rules, combinator }` and an item with a `tags` array
and returns a boolean. An empty rule list matches every item.
Combinator `all` requires every rule to pass; `any` short-circuits
on the first success. The switch over rule kinds is exhaustive for
the tag-only v1 set; the rule-widening follow-up extends the switch.
Unit tests cover every rule kind, empty rules, both combinators,
multi-tag items, untagged items, and an `any` combinator that
combines an `untagged` rule with a `tag-includes` rule.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`SmartViewsSidebar` renders a 240px-wide rail with the current user's smart views for a given resource. The empty state nudges toward the "+ New Smart View" affordance; the populated state shows each view as an UnstyledButton with optional icon, name, and a kebab Menu (Edit / Delete). The Delete flow uses the existing `useConfirm` for parity with the dashboard / saved-search delete flows on the same page. `SmartViewEditorDrawer` is a Mantine Drawer that opens on the right and accepts: name, optional icon (free text, intended for an emoji or short symbol), top-level combinator (`all` | `any`), and a dynamic rule list. Each rule row picks a kind from the discriminated union and (for the tag-includes / tag-excludes kinds) a tag from the available-tags pool. Saving calls `useCreateSmartView` for a fresh view and `useUpdateSmartView` for an existing one. Draft rules with an empty tag are dropped on save, so the editor stays forgiving while the persisted shape stays clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o the listing page
DashboardsListPage now pulls smart views for the dashboard resource
and renders the sidebar in a left rail next to the existing listing
column. A new nuqs `?view=<id>` state drives the active view;
clicking a sidebar entry sets it and clicking the active entry
again clears it (shareable URL).
The `filteredDashboards` memo factors the active view's rules
through `evaluateSmartView` AFTER the manual tag and search
filters, so manual chips and the view's rule list AND-combine. The
existing favorites + preset sections sit above the listing and are
unchanged.
`SmartViewEditorDrawer` renders at the page level via
`useDisclosure`; the sidebar's "+ New" and per-item Edit open it
with the right initial state. The drawer reuses the page's
`allTags` memo as the tag pool so a user only picks from tags
already in their dashboards.
Extends the existing RTL test with a fourth case: when the mocked
`useQueryState('view')` returns a known smart-view id, only items
that pass that view's rules show up in the grid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 5428dd8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…AL_MODE builds work useSmartViews / useCreateSmartView / useUpdateSmartView / useDeleteSmartView all unconditionally hit the API. On the Vercel preview and any `IS_LOCAL_MODE` build there is no `/smart-views` backend, so GETs 504 (sidebar shows "Loading..." for ~7s while React Query retries before falling through to the empty state) and POST/PATCH/DELETE 504 too (the editor drawer shows "Failed to create smart view" but keeps the user staring at a useless modal). Mirror the pattern used by `favorites.ts` and `dashboard.ts`: each hook short-circuits to `createEntityStore<SmartView>( 'hdx-local-smart-views')` when `IS_LOCAL_MODE` is true. The listing is filtered + sorted by `ordering` on the read path. The React Query invalidation logic is unchanged so the sidebar refreshes on create / update / delete. Drive-by: switch the editor drawer's Cancel button from `variant="default"` to `variant="secondary"` to satisfy `agent_docs/code_style.md`'s Button variant rule (caught by `no-restricted-syntax` after rebuild). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed a fix in commit Fix mirrors the pattern in Verified on the redeployed preview:
Drive-by: switched the editor drawer's Cancel button from |
…ed view does not crash the listing Before this commit, a SmartView document with `rules` undefined or null (server response that dropped a field, localStorage entry written by an earlier draft, a `Mixed` Mongoose column that returned a stray shape) crashed the listing in three places: - `evaluateSmartView(view, item)` called `view.rules.length` and `view.rules.every`, throwing "Cannot read properties of undefined / null". - `SmartViewEditorDrawer` seeded its draft from `existingView.rules.length`, throwing on open of the Edit menu. - The unrelated nuqs URL state would still leave `?view=<id>` pointing at a view whose rules now blow up the filter memo, taking the entire page down with the Next.js client-side exception boundary. Coerce defensively at every entry point: - `evaluateSmartView` accepts `rules?: SmartViewRule[] | null` and treats anything non-array as empty (-> match-all). Same for `combinator`, which defaults to `all` if missing. Entries inside the array that are not objects with a `kind` field are filtered out before evaluation. - `SmartViewEditorDrawer` seeds its draft via the same `Array.isArray` check + null-entry filter; missing `combinator` also defaults to `all`. - `smartView.ts` exposes a private `normalizeSmartView` that runs on both the local-mode and server-mode read paths so the data React Query hands back to consumers always has the canonical shape (id/name/resource/combinator/ordering present, rules guaranteed array, isShared optional). Added a regression test on `evaluateSmartView` for the empty-view (rules undefined) and the explicit-null-rules cases. Root-cause notes: the user's repro stack pointed at `Cannot read properties of null (reading 'value')` inside a useState chain. The exact `value` access lives inside Mantine's Drawer / Select internals when a rule prop becomes null mid-render; the upstream cause is the same `rules` shape mismatch fixed here.
|
Pushed Repro (verified on the redeployed preview before the fix): pre-seed Three call sites called Fix is defensive parsing on every entry point:
Reproduced and verified on the Vercel preview after the fix:
|
…e synthetic-event crash
Typing into the smart-view editor's Name or Icon field crashed the
listing page after a few keystrokes with `TypeError: Cannot read
properties of null (reading 'value')` inside a useState update.
Root cause: both TextInput onChange handlers closed over the
synthetic event INSIDE a `setDraft(d => ({...d, name:
e.currentTarget.value}))` updater. React 18 nulls out
`event.currentTarget` after the event handler returns (and React
Compiler / concurrent rendering routinely defers / re-runs the
updater function). When the updater finally executes the
event-detach has already happened and `e.currentTarget` is null;
reading `.value` on it throws and Next.js's client-side exception
boundary takes the whole page down.
Fix: capture `e.currentTarget.value` into a local const inside the
synchronous event handler, then pass the const to the updater.
Reproducer that crashed reliably on the Vercel preview before this
commit: open `/dashboards/list`, click "+ New Smart View", type
into the Name field via real keystrokes (not a single setValue);
the page shows the Next.js client-side exception screen after a
few characters. Same pattern crashed the Icon field.
Documented in a code comment so the next person who writes
`onChange={e => setX(prev => ({...prev, foo: e.currentTarget.value}))}`
in this file doesn't re-introduce the same bug.
|
Pushed Reproducer: open Root cause: both TextInput onChange handlers in onChange={e => setDraft(d => ({ ...d, name: e.currentTarget.value }))}React 18 nulls out Fix: capture Verified on the redeployed preview: typed |
…, accent active state
Polish for scale to hundreds of dashboards.
- Default `All Dashboards` row at the top of the sidebar. Always
active when no smart view is selected, single-click clears any
active view, and shows the total dashboard count so the catalog
scope is legible at a glance.
- Count badge per smart view (`<name> <count>`) computed against
the same `dashboards` reference that drives the grid, so the
badge and the visible result set move together.
- Stronger active state: 3px inset accent bar on the left edge
(matches the AppNav rail) plus a subtle background tint and the
label switches to weight 600. Reads as `you are here` from
several feet away rather than a fragile bold-only cue.
- Quieter empty state: drop the `No smart views yet. Pin a tag
filter combination to jump back to it.` paragraph. Now the
sidebar shows the `SMART VIEWS` section header with its inline
`+` affordance and a single subtle row-shaped `+ New Smart
View` button. Nothing nags the user when no rules are
configured.
- Density bump: 6/10px padding per row, 220px rail width (down
from 240). Tighter than the previous gap-heavy layout and
closer to professional catalog rails (Linear, Notion, Datadog
monitor lists).
- Layout alignment: the page now wraps the sidebar + main column
in a single `Container maw={1440}` so the rail no longer floats
far-left while the content sits in a separately-centered 1200px
column. Removes the visual `gap of dead space` between the two.
Sidebar is now a pure presentation component: counts and total
come in as props from the listing page, so the same evaluator
output drives both the grid and the badges. Keeps the component
reusable for Saved Searches in the followup PR-6 without an
internal `dashboards` import.
|
Pushed What changed:
Sidebar is now a pure presentation layer. Counts and total come in as props from the listing page ( Out-of-scope for this PR but worth flagging for the catalog-at-scale direction:
Verified on the redeployed preview with a 5-dashboard / 2-smart-view seed mirroring the screenshot you shared. |
Summary
Save reusable filter combinations as named smart views pinned to a left-rail sidebar on the Dashboards listing page. Rules in v1 are tag-only (
tag includes X,tag excludes Y,is untagged) with anall/anycombinator. Clicking a view applies its rules to the listing and shares the URL via?view=<id>; clicking it again clears the active view. Edit and delete actions live on a kebab menu per sidebar entry.This PR is chained on #2379. Please review #2379 first; the Vercel preview here shows the cumulative UX (multi-select chips + smart-views sidebar working together). After #2379 merges I'll rebase this onto
mainand re-target the base.Why this shape
The 1460-line diff classifies as Tier 4 by the auto-triager, but the history is structured into six self-contained commits so a reviewer can read each layer in isolation rather than a single big blob:
feat(common-utils): SmartView Zod schemas(52 lines). Schema + types only.feat(api): SmartView model + CRUD router(452 lines). Mongoose model mirrorsfavorite.ts; controller + router mirrorrouters/api/favorites.ts. Router mounted at/smart-views.feat(app): useSmartViews hook + pure evaluateSmartView function(207 lines). React Query hook + pure rule evaluator with unit tests.feat(app): SmartViewsSidebar and SmartViewEditorDrawer components(497 lines). Sidebar (240px rail) + Mantine Drawer-based editor.feat(dashboards): wire Smart Views sidebar + apply-view URL state into the listing page(88 net lines). Touchpoints only inDashboardsListPage.tsx; the existing favorites + presets stay above the listing.If a smaller-PR shape is preferred, the natural split is to land 1-3 on their own (backend + plumbing, no UI) and follow up with 4-5 as a UI PR. I left it as a single PR so the Vercel preview demonstrates the full flow, but happy to split if the reviewer would rather take it in two passes.
What's in v1
tag-includes,tag-excludes,untagged).all|any); no nested rule groups.?view=<id>URL state combines with?tags=...and?search=...(AND-combined).savedSearch; Saved Searches sidebar parity drops in without a schema change.What's deferred
isSharedUI), nested rule groups.I'll file tracking issues for each before this is marked ready.
Test plan
yarn jestonpackages/app(the new RTL test for the active-view filter is inDashboardsListPage.test.tsx).yarn jestonpackages/common-utils(no schema-test additions needed; the api router tests round-trip the schema end-to-end).yarn ci:lintclean onpackages/appandpackages/api.yarn tsc --noEmitclean on both packages.packages/api/src/routers/api/__tests__/smartViews.test.ts): will run in CI; covers POST -> GET round-trip, resource discriminator, 404 on cross-user access, validation rejection on bad rule kinds.?view=<id>URL in a new tab.