feat(dashboards): views prototype (demo-only, chained on #2379)#2382
feat(dashboards): views prototype (demo-only, chained on #2379)#2382alex-fedotyev wants to merge 14 commits into
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: a06531b 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. |
Mechanical rename. No behavior change. Same routes (renamed to
/list-views), same wire shape, same combinator + tag rule kinds.
* common-utils: SmartView* Zod symbols become ListView*. The
intermediate SmartViewTagRuleSchema alias collapses into
ListViewRuleSchema directly so PR-3's union widening edits one
symbol instead of two.
* api: smartView.ts model and controllers rename to listView.ts;
router file rename to listViews.ts and mount point change to
/list-views. Mongoose model name becomes 'ListView' so the
default collection name becomes 'listviews'.
* app: smartView.ts hook file, evaluateSmartView util, and
SmartViewsSidebar component directory all rename. React Query
keys and localStorage keys swap to 'list-views' / 'hdx-local-
list-views'. User-visible copy ("Smart View", "smart view")
becomes "View" / "view"; the sidebar header reads "Views" and
the empty-state button reads "+ New View".
Existing tests round-trip unchanged after path / symbol updates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ive-alerts / created-by-me The discriminated union picks up three non-tag kinds: * updated-within-days (numeric `days`, 1-365): matches items whose `updatedAt` is younger than the window. Editor renders a NumberInput with 1 / 7 / 30 preset pills. * has-active-alerts: zero-config rule. The evaluator stays pure; the listing precomputes per-item alert state and passes it in via `context.itemHasActiveAlerts`. * created-by-me: zero-config rule. Matches on the current user's `_id` or, if missing, on email. The listing pulls identity from `api.useMe()` and feeds it into the evaluator context. The widening is additive; existing tag-only documents keep parsing because every rule still carries a discriminant `kind`. The Mongoose model stores `rules` as Mixed, so the schema flows through with no migration. The router round-trip test now covers POST -> GET for a mixed-kind view in one toMatchObject assertion against a canonical config, and rejects out-of-range `days`. Evaluator gains 9 new unit tests across the three kinds. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three quick-filter pills sit between the search / tag row and the listing. Each independently AND-combines with the rest of the filter chain (search, tag chips, active list view). * Recently updated: pill opens a popover with 1 / 7 / 30 presets. Default to 7 days when first toggled on. URL: ?recentDays=<n>. * With active alerts: zero-config toggle. URL: ?withAlerts=1. * Created by me: zero-config toggle. URL: ?createdByMe=1. The semantics route through evaluateListView so the pill behavior matches what the save flow (next commit) will persist into a ListView rule. Empty state copy widens to include any active pill so a no-match state still reads as "no matches" rather than "no dashboards yet". Two new unit tests cover the created-by-me and recently-updated pills filtering the visible grid. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Active-filter row now ends with a "Save as view" button. Disabled until the user has at least one chip, pill, or quick filter active; tooltip explains why. Click opens a modal that converts the current filter state into a ListView rule array: * Each tag chip becomes a tag-includes rule. * recentDays becomes updated-within-days. * withAlerts becomes has-active-alerts. * createdByMe becomes created-by-me. * Search query is intentionally NOT persisted (transient). * Combinator is `all` (chips + pills both narrow); the advanced drawer remains the entry point for `any`. On save the modal clears the transient filter state and routes to ?view=<new-id>, so the user sees the saved view applied rather than the now-duplicated raw filters. The sidebar's inline "+" primary button is gone. A kebab on the Views section header keeps the path to the advanced editor drawer (hand-written rule lists) for power users. 5 new unit tests cover buildRulesFromFilters end-to-end across every combination of inputs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A new "Suggested" section in the sidebar pins four non-editable system views above the user's saved views: * My dashboards (created-by-me) * Recently updated (updated-within-days = 7) * With active alerts (has-active-alerts) * Untagged System view ids carry a `system:` prefix so they never collide with API-returned ids. The listing's lookup checks system ids first, then falls through to the user-views response. View counts on the sidebar now include the system rail in the same pass so the suggested counts stay in sync with whatever the grid is rendering. Saved-search system views drop `has-active-alerts` until PR-6 lands the saved-search alert analogue; otherwise the set matches the dashboard rail. 8 new unit tests on getDefaultListViews + isSystemViewId. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Save reusable filter combinations as named "views" pinned to a
left-rail sidebar on the Dashboards listing page. The PR is being
repurposed as a UX prototype that demonstrates the end-state
story before the production split.
This PR is chained on #2379. Please review #2379 first; the
Vercel preview here shows the cumulative UX (multi-select chips +
ListViews sidebar working together). After #2379 merges I'll
rebase this onto
main.What this prototype demos
ListViewend-to-end; theuser-facing copy is just "view" / "Views".
untagged, updated-within-days, has-active-alerts, created-by-me.
Stored as an additive Zod discriminated union.
rule kinds. Each pill syncs to its own nuqs URL key so a
filtered listing stays shareable.
lives next to the chips and pills, not in the sidebar header.
A modal converts the active filter state into a rules array
and routes to ?view= on save.
the user's saved views: My dashboards, Recently updated, With
active alerts, Untagged. Counts update in lockstep with the
grid.
the Views section header for hand-written rule lists.
Why a throwaway prototype branch
Tier-4 by the classifier (+800 / -250 LOC on top of the existing
1775 / 230). Acceptable because the goal here is to evaluate the
integrated UX in one Vercel preview, then disassemble. After
UX agreement lands:
filters-first save. Fresh branch from main.
fold into PR-2-final.
What's deferred
Test plan
yarn jeston@hyperdx/app(33 tests covering theevaluator, listing pills, save modal, default views).
yarn jeston@hyperdx/common-utils(1169 pass).yarn tsc --noEmitclean on api + app.yarn ci:lintclean on api + app.(
packages/api/src/routers/api/__tests__/listViews.test.ts):will run in CI; covers POST/GET round-trips for tag and
non-tag rules, 404s, validation rejection.
(feat(dashboards): multi-select tag filter on Dashboards and Saved Searches #2379 + this PR): light + dark theme, seed dashboards
with mixed tagging, alerts, and createdBy; exercise each
pill independently and combined; save a view via the new
modal and verify it lands in the sidebar with the right
count and round-trips through edit + delete; click each
system view and confirm the ?view=system:* URL shape.