[Dashboard] Redefine the user page with tabs and updated UI#1351
[Dashboard] Redefine the user page with tabs and updated UI#1351madster456 merged 38 commits intodevfrom
Conversation
Made-with: Cursor # Conflicts: # apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRefactors the user page to design-system primitives, adds tabbed layout, client analytics and payments sections, implements a per-user activity heatmap (backend route + client hook), extends transaction APIs with customerId, tightens metadata typing, and adds E2E tests for the activity endpoint. Changes
Sequence Diagram(s)sequenceDiagram
participant AdminUI as Admin Client
participant Internals as Admin Internals (useUserActivity)
participant Backend as Backend /internal/user-activity
participant ClickHouse as ClickHouse (analytics_internal.events)
participant ActivityUI as ActivityGraph / Suspense
AdminUI->>Internals: call useUserActivity(userId)
Internals->>Backend: GET /internal/user-activity?user_id={userId}
Backend->>ClickHouse: query daily event counts (fixed UTC window)
ClickHouse-->>Backend: rows per day or error
Backend-->>Internals: { data_points: [...] }
Internals-->>AdminUI: UserActivityResponse
AdminUI->>ActivityUI: render (Suspense -> ActivityGraph)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR refactors the user detail page in the dashboard to use the shared
Confidence Score: 4/5Safe to merge after fixing silent error handling in RestrictionDialog; the two P1s leave users with no feedback when restriction operations fail. Two P1 findings exist: unhandled rejections in handleRemoveRestriction and silent error swallowing in handleSaveAndRestrict. Both affect the restriction management flow. The remaining findings (oauthColumns useMemo, runAsynchronouslyWithAlert on menu items) are P2. Score is 4 pending the P1 fixes. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx — specifically the RestrictionDialog handlers and OAuthProvidersSection Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[UserPage] --> B[RestrictionBanner]
A --> C[UserHeader\nDesignMenu actions]
A --> D[DesignCategoryTabs]
D --> E{selectedTab}
E -->|profile| F[UserDetails\nDesignEditableGrid]
E -->|profile| G[ContactChannelsSection\nDesignDataTable]
E -->|profile| H[UserTeamsSection\nDesignDataTable]
E -->|profile| I[OAuthProvidersSection\nDesignDataTable]
E -->|profile| J[MetadataSection]
E -->|analytics| K[TabPlaceholder]
E -->|payments| L[TabPlaceholder]
E -->|fraud-protection| M[TabPlaceholder]
C -->|impersonate / remove-2fa| N[async onClick\n⚠ no runAsynchronouslyWithAlert]
F --> O[RestrictionDialog]
O -->|Save & restrict| P[handleSaveAndRestrict\n⚠ silent catch]
O -->|Remove restriction| Q[handleRemoveRestriction\n⚠ no catch block]
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (3)
125-131:satisfiesonly covers the truthy branch; move it to assert the whole items array.
satisfies DesignMenuActionItem[]is applied inside the conditional spread, so neither the static items nor the empty-array branch are type-checked againstDesignMenuActionItem[]. If you want the assertion to protect the entire list, place it on the outeritemsarray (or type the intermediate conditional asDesignMenuActionItem[]on both branches).♻️ Proposed shape
- items={[ + items={([ { id: "impersonate", /* ... */ }, - ...user.isMultiFactorRequired ? [{ - id: "remove-2fa", - /* ... */ - }] satisfies DesignMenuActionItem[] : [], + ...(user.isMultiFactorRequired ? [{ + id: "remove-2fa", + /* ... */ + }] : []), { id: "delete", /* ... */ }, - ]} + ] satisfies DesignMenuActionItem[])}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx around lines 125 - 131, The conditional currently applies "satisfies DesignMenuActionItem[]" only to the truthy spread branch (the array with id "remove-2fa"), so the overall items list (including the [] branch) is not type-checked; fix this by moving the satisfies assertion to the outer items array (or explicitly type both branches as DesignMenuActionItem[]), e.g. ensure the final items array that includes "...user.isMultiFactorRequired ? [{ id: 'remove-2fa', label: 'Remove 2FA', onClick: async () => { await user.update({ totpMultiFactorSecret: null }); }, }] : []" is asserted with "satisfies DesignMenuActionItem[]" so the whole list (not just the truthy branch) is validated against DesignMenuActionItem.
1355-1402: Consider persisting the selected tab in the URL.
selectedTablives only in component state, so refreshing or sharing a link always lands on the Profile tab. For a multi-tab user detail page this is usually undesirable (especially once Analytics/Payments/Fraud-Protection become real content). Consider syncing with a query param (e.g.?tab=analytics) viauseSearchParams+router.replace, or storing insessionStorageas a minimum.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx around lines 1355 - 1402, Persist the UserPage selectedTab state to the URL so tab survives refresh/share: update the UserPage component to initialize selectedTab from a query param (e.g., ?tab=...) and keep it in sync on changes by calling setSelectedTab when the query param changes; on tab clicks call router.replace (or update useSearchParams) to set the tab param instead of only setSelectedTab, and fall back to USER_PAGE_TABS[0] if the param is invalid; alternatively, if URL sync is undesirable, persist selectedTab to sessionStorage on change and restore it on mount; touch symbols: UserPage, selectedTab, setSelectedTab, USER_PAGE_TABS, useSearchParams/router.replace or sessionStorage to implement this.
1171-1243:oauthColumnsshould be memoized like the other column definitions.
contactChannelColumns(line 740) andteamColumns(line 906) are wrapped inuseMemo, butoauthColumnsis recreated on every render. This produces a newcolumnsreference forDesignDataTableon each render, which typically defeats TanStack Table's memoized row models and can cause visible churn. Please make this consistent.♻️ Proposed fix
- const oauthColumns: ColumnDef<ServerOAuthProvider>[] = [ + const oauthColumns = useMemo<ColumnDef<ServerOAuthProvider>[]>(() => [ ... - ]; + ], [handleProviderUpdate]);Note that
handleProviderUpdateitself is recreated each render; if you want full stability, wrap it inuseCallbackor inline it into the memo.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx around lines 1171 - 1243, The oauthColumns definition is not memoized which causes a new columns reference each render; wrap the oauthColumns array in useMemo (like contactChannelColumns and teamColumns) to return a stable reference for DesignDataTable, e.g. const oauthColumns = useMemo(() => [...], [/* dependencies */]); and include handleProviderUpdate and any props/state used inside the column cells in the dependency array (or stabilize handleProviderUpdate with useCallback) so the memo remains correct and avoids unnecessary re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 939-943: The onClick handler currently builds the URL with string
interpolation using stackAdminApp.projectId and row.original.id; update it to
construct the URL with urlString`...` or by wrapping dynamic parts with
encodeURIComponent() (e.g., encodeURIComponent(stackAdminApp.projectId) and
encodeURIComponent(row.original.id)) before passing to window.open in the
onClick for the "View Team" item so the URL follows the repository guideline for
safe/consistent URL construction.
- Around line 1298-1335: ActivityPlaceholder currently seeds random values
during render inside useMemo using Math.random (cells), causing server/client
hydration mismatches; change it to generate the random cells only after mount by
replacing the useMemo/Math.random usage with a client-only initialization (e.g.,
useState + useEffect or a mounted flag) so cells are populated in an effect
after the component mounts (still referencing ACTIVITY_GRID_WEEKS and
ACTIVITY_GRID_DAYS and keeping the same className grid rendering), ensuring the
initial server HTML matches and preventing React hydration warnings.
- Around line 1366-1373: The code is unsafely casting the onSelect value to
UserPageTab; update the onSelect handler for DesignCategoryTabs to
validate/narrow the incoming id against the known USER_PAGE_TABS array instead
of using "as UserPageTab". Implement a runtime check that confirms id is one of
the entries in USER_PAGE_TABS (e.g., find/includes) and only call
setSelectedTab(id) when it matches; otherwise ignore or handle the unexpected
value (fallback to a default or no-op). Ensure selectedTab and setSelectedTab
continue to use the UserPageTab type so the value stored is type-safe.
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 125-131: The conditional currently applies "satisfies
DesignMenuActionItem[]" only to the truthy spread branch (the array with id
"remove-2fa"), so the overall items list (including the [] branch) is not
type-checked; fix this by moving the satisfies assertion to the outer items
array (or explicitly type both branches as DesignMenuActionItem[]), e.g. ensure
the final items array that includes "...user.isMultiFactorRequired ? [{ id:
'remove-2fa', label: 'Remove 2FA', onClick: async () => { await user.update({
totpMultiFactorSecret: null }); }, }] : []" is asserted with "satisfies
DesignMenuActionItem[]" so the whole list (not just the truthy branch) is
validated against DesignMenuActionItem.
- Around line 1355-1402: Persist the UserPage selectedTab state to the URL so
tab survives refresh/share: update the UserPage component to initialize
selectedTab from a query param (e.g., ?tab=...) and keep it in sync on changes
by calling setSelectedTab when the query param changes; on tab clicks call
router.replace (or update useSearchParams) to set the tab param instead of only
setSelectedTab, and fall back to USER_PAGE_TABS[0] if the param is invalid;
alternatively, if URL sync is undesirable, persist selectedTab to sessionStorage
on change and restore it on mount; touch symbols: UserPage, selectedTab,
setSelectedTab, USER_PAGE_TABS, useSearchParams/router.replace or sessionStorage
to implement this.
- Around line 1171-1243: The oauthColumns definition is not memoized which
causes a new columns reference each render; wrap the oauthColumns array in
useMemo (like contactChannelColumns and teamColumns) to return a stable
reference for DesignDataTable, e.g. const oauthColumns = useMemo(() => [...],
[/* dependencies */]); and include handleProviderUpdate and any props/state used
inside the column cells in the dependency array (or stabilize
handleProviderUpdate with useCallback) so the memo remains correct and avoids
unnecessary re-renders.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b73d132f-3aca-436a-9f0d-e0535555ee6f
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)
1144-1174:⚠️ Potential issue | 🟠 MajorUse alerts for failed provider toggles instead of toast-only errors.
A missed toast here leaves the user thinking the sign-in / connected-accounts change succeeded when it did not. These failures should go through an alert-based path rather than
toast(...).As per coding guidelines, "For blocking alerts and errors, never use
toast, as they are easily missed by the user. Instead, use alerts".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx around lines 1144 - 1174, The error branch inside handleProviderUpdate currently uses toast(...) which can be missed; replace the toast-based error notifications with the app's alert-based error flow (e.g., call the existing alert/Modal/confirm API used elsewhere like openAlert/showAlert) for both the KnownErrors.OAuthProviderAccountIdAlreadyUsedForSignIn case and the generic error case, passing the same title/description/variant content so failures are surfaced as blocking alerts rather than toasts; keep the success branch using toast and do not change the function signature of handleProviderUpdate.
♻️ Duplicate comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)
1386-1401:⚠️ Potential issue | 🟠 MajorHeatmap cells are still mouse-only.
TooltipTriggeris wrapping anaria-hiddendiv, so keyboard and screen-reader users still cannot reach a cell or hear its date/count. Use a focusable element (for examplebutton type="button") and give it anaria-labelwith the formatted date and activity count.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx around lines 1386 - 1401, Replace the non-focusable, aria-hidden div inside TooltipTrigger with a focusable element (e.g., a button type="button") so keyboard and screen-reader users can reach each heatmap cell; keep the same classes (including ACTIVITY_COLORS[level]) and remove aria-hidden, and add an aria-label that combines formatActivityDate(cell.date) and the activity count (e.g., using cell.activity to render "1 event" vs "N events") so the TooltipTrigger, TooltipContent, and formatActivityDate usage remain consistent and accessible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 1144-1174: The error branch inside handleProviderUpdate currently
uses toast(...) which can be missed; replace the toast-based error notifications
with the app's alert-based error flow (e.g., call the existing
alert/Modal/confirm API used elsewhere like openAlert/showAlert) for both the
KnownErrors.OAuthProviderAccountIdAlreadyUsedForSignIn case and the generic
error case, passing the same title/description/variant content so failures are
surfaced as blocking alerts rather than toasts; keep the success branch using
toast and do not change the function signature of handleProviderUpdate.
---
Duplicate comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 1386-1401: Replace the non-focusable, aria-hidden div inside
TooltipTrigger with a focusable element (e.g., a button type="button") so
keyboard and screen-reader users can reach each heatmap cell; keep the same
classes (including ACTIVITY_COLORS[level]) and remove aria-hidden, and add an
aria-label that combines formatActivityDate(cell.date) and the activity count
(e.g., using cell.activity to render "1 event" vs "N events") so the
TooltipTrigger, TooltipContent, and formatActivityDate usage remain consistent
and accessible.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 71c342aa-317e-4210-8fc4-6893a2ab92df
📒 Files selected for processing (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsxapps/dashboard/src/lib/stack-app-internals.tspackages/stack-shared/src/interface/admin-interface.tspackages/stack-shared/src/interface/admin-metrics.tspackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/stack-shared/src/interface/admin-metrics.ts
|
Disable emails tab in user profile |
Summary by CodeRabbit
New Features
UI/UX Improvements
API
Tests