Conversation
Add Prisma schema model and migration for user-scoped custom dashboard views with panel selection, optional filters, and sort ordering.
Add listViews, createView, updateView, and deleteView tRPC procedures to the dashboard router for managing user-scoped custom dashboard views.
Add a tab bar at the top of the dashboard for switching between the default view and user-created custom views. Includes a view builder dialog for selecting panels and a custom view component that renders the chosen charts and summary cards in a responsive grid.
Document the custom view workflow: creating, switching, editing, and deleting views, with a stepper guide and panel reference table.
Greptile SummaryThis PR adds a user-scoped custom dashboard views feature to the existing dashboard page, allowing users to persist named subsets of dashboard panels with optional filter presets. The implementation covers the full stack: a new Key observations:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User (Browser)
participant P as page.tsx
participant VB as ViewBuilderDialog
participant CV as CustomView
participant R as dashboard router (tRPC)
participant DB as PostgreSQL
U->>P: Load dashboard
P->>R: listViews (protectedProcedure)
R->>DB: SELECT * FROM DashboardView WHERE userId = ?
DB-->>R: DashboardView[]
R-->>P: views[]
P-->>U: Render tab bar (Default + custom tabs)
U->>P: Click "+ New View"
P->>VB: open=true
U->>VB: Enter name + select panels → Save
VB->>R: createView (VIEWER + withAudit)
R->>DB: aggregate(_max sortOrder) → INSERT DashboardView
DB-->>R: new DashboardView
R-->>VB: created view
VB->>P: invalidateQueries listViews + close
U->>P: Click custom view tab
P->>CV: render CustomView(view)
CV->>R: chartMetrics / stats / pipelineCards (conditional)
R->>DB: query metrics
DB-->>R: data
R-->>CV: panel data
CV-->>U: Render selected panels
U->>P: Click delete icon → confirm
P->>R: deleteView (VIEWER + withAudit)
R->>DB: SELECT + DELETE DashboardView WHERE id = ?
DB-->>R: ok
R-->>P: {deleted: true}
P->>P: invalidateQueries + conditionally reset activeView
Last reviewed commit: 9998bfc |
Previously, deleting any custom view would unconditionally switch back to the Default tab. Now only resets if the deleted view was active.
- Replace count-based sortOrder with aggregate max to avoid TOCTOU race
- Add withTeamAccess("VIEWER") to createView, updateView, deleteView
- Add withAudit middleware to all three view mutations
- Pass environmentId through frontend for team context resolution
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| if (confirm(`Delete "${view.name}"?`)) { | ||
| deleteMutation.mutate({ environmentId: selectedEnvironmentId!, id: view.id }); |
There was a problem hiding this comment.
Non-null assertion passes null to Zod z.string() schema
selectedEnvironmentId is typed as string | null | undefined from the environment store. Using selectedEnvironmentId! here does not coerce null to a string — TypeScript's ! only suppresses the compiler's null check. At runtime, if no environment is selected, null is passed as the environmentId and Zod's z.string() validator will reject it with a BAD_REQUEST, causing a silent mutation failure.
The create and update flows correctly handle this by using selectedEnvironmentId ?? "" (e.g., lines 419 and 426). The same fallback should be applied here for consistency:
| deleteMutation.mutate({ environmentId: selectedEnvironmentId!, id: view.id }); | |
| deleteMutation.mutate({ environmentId: selectedEnvironmentId ?? "", id: view.id }); |
Because listViews requires no environment context, it's possible for a user to have the tab bar rendered with views while selectedEnvironmentId is still null, making this a realistic failure path rather than a theoretical one.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(dashboard)/page.tsx
Line: 190
Comment:
**Non-null assertion passes `null` to Zod `z.string()` schema**
`selectedEnvironmentId` is typed as `string | null | undefined` from the environment store. Using `selectedEnvironmentId!` here does not coerce `null` to a string — TypeScript's `!` only suppresses the compiler's null check. At runtime, if no environment is selected, `null` is passed as the `environmentId` and Zod's `z.string()` validator will reject it with a `BAD_REQUEST`, causing a silent mutation failure.
The create and update flows correctly handle this by using `selectedEnvironmentId ?? ""` (e.g., lines 419 and 426). The same fallback should be applied here for consistency:
```suggestion
deleteMutation.mutate({ environmentId: selectedEnvironmentId ?? "", id: view.id });
```
Because `listViews` requires no environment context, it's possible for a user to have the tab bar rendered with views while `selectedEnvironmentId` is still `null`, making this a realistic failure path rather than a theoretical one.
How can I resolve this? If you propose a fix, please make it concise.* fix: use storeKey instead of undefined componentKey in LiveTailPanel The merge of custom dashboards (#36) and live tail (#30) created a conflict where LiveTailPanel references `componentKey` which was never destructured as a local variable. The correct reference is `storeKey`, derived from selectedNode.data.componentKey at the top of the component. * docs: update OIDC group sync documentation Rewrite the OIDC role/team mapping section to reflect the new group sync toggle, separate scope/claim fields, and stepper-based setup guide. Removes references to legacy Admin/Editor Groups fields.
Summary
DashboardViewPrisma model with migration for storing user-scoped custom dashboard views (panels, filters, sort order)listViews,createView,updateView,deleteView) to the dashboard router with ownership checksViewBuilderDialogcomponent with panel selection (10 panels across Pipeline, System, and Summary categories)CustomViewcomponent that renders selected panels in a responsive 2-column grid with the same time range picker and filter barTest plan
npx prisma migrate devagainst a database to apply the migration