Skip to content

fix: Format pagination numbers in log footers#3969

Closed
naaa760 wants to merge 21 commits intounkeyed:mainfrom
naaa760:fix/log-footer
Closed

fix: Format pagination numbers in log footers#3969
naaa760 wants to merge 21 commits intounkeyed:mainfrom
naaa760:fix/log-footer

Conversation

@naaa760
Copy link

@naaa760 naaa760 commented Sep 15, 2025

What does this PR do?

Implements proper number formatting for pagination counts in log footers across the dashboard. Large numbers like 1239436 are now displayed as 1,239,436 (or locale-appropriate formatting) making them much more readable for users.

  • Applied new Intl.NumberFormat().format(number) to all "Showing X of Y" pagination text in:
  • Main logs table
  • Key details logs table
  • Ratelimit logs tables
  • Virtual table loading indicator
  • API lists
  • Projects list
  • Root keys table
  • Deployments table
  • Authorization roles & permissions tables

fixes #3922

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Navigate to any logs page with large numbers (>1000 entries)
  • Verify pagination footer displays formatted numbers (e.g., "Showing 100 of 1,239,436 requests")
  • Test across different table components (APIs, keys, logs, deployments, etc.)
  • Verify numbers are formatted consistently across all pagination footers

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Summary by CodeRabbit

  • New Features

    • Multi-select in Keys list with row checkboxes, header selection controls, and per-row loading states.
    • Locale-aware full number formatting applied across lists, tables, headers, and footers.
  • Style

    • Identity names in Keys list are now linked; spacing and empty-state text wrapping refined.

@changeset-bot
Copy link

changeset-bot bot commented Sep 15, 2025

⚠️ No Changeset found

Latest commit: 7cb8de3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Sep 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
engineering Ready Ready Preview Comment Sep 19, 2025 5:29pm

@vercel
Copy link

vercel bot commented Sep 15, 2025

@naaa760 is attempting to deploy a commit to the Unkey Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Warning

Rate limit exceeded

@naaa760 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 3 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a01a4c2 and 9e3b50e.

📒 Files selected for processing (5)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (13 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (2 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2 hunks)
  • apps/dashboard/lib/fmt.ts (1 hunks)
📝 Walkthrough

Walkthrough

Adds a new non-compact number formatter formatNumberFull and applies it across many dashboard list/table components to format "Showing/Viewing X of Y" counts; introduces multi-select row selection and related UI in KeysList. No public API deletions; most other edits are presentational/formatting.

Changes

Cohort / File(s) Change summary
KeysList multi-select
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
Adds multi-select state (selectedKeys: Set), hover navigation state (hoveredKeyId, isNavigating), per-row checkboxes shown on hover/selection, getSelectedKeysState aggregate, integrates SelectionControls in header, and shows a Loading indicator in identity/icon while navigating. UI/state changes only; exported component signature unchanged.
Number formatting applied across UI
apps/dashboard/app/(app)/apis/_components/api-list-client.tsx, apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx, apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx, apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx, apps/dashboard/app/(app)/logs/components/table/logs-table.tsx, apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx, apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx, apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx, apps/dashboard/components/virtual-table/components/loading-indicator.tsx, apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx, apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx, apps/dashboard/app/(app)/projects/_components/list/index.tsx
Imports and uses formatNumberFull(...) to render counts (e.g., apiList.length, total, Showing X of Y, load-more footer counts) instead of raw numbers. Mostly JSX reflow/formatting and minor spacing tweaks; behavior and public APIs unchanged.
Number formatting helper added
apps/dashboard/lib/fmt.ts
Adds exported formatNumberFull(n: number): string using Intl.NumberFormat('en-US') (non-compact). Existing compact formatNumber remains.
Minor typing/formatting tweaks
apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx, apps/dashboard/components/virtual-table/components/loading-indicator.tsx
Small internal typing change for a render helper parameter, reflowed JSX blocks, wrapped count text in spans, and used formatNumberFull. No behavioral or API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant KL as KeysList
  participant R as TableRow
  participant SC as SelectionControls

  U->>R: Hover row
  R-->>U: Reveal checkbox UI
  U->>R: Click row checkbox
  R->>KL: toggle key id in selectedKeys
  KL->>KL: compute getSelectedKeysState(selectedKeys, keys)
  KL->>SC: update selectedKeys, setter, aggregate state
  SC-->>U: reflect header selection (all/mixed/none)
  U->>R: Click row to navigate
  R->>KL: set isNavigating
  KL-->>R: show Loading indicator in identity/icon
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Dashboard, Improvement, Needs Approval

Suggested reviewers

  • mcstepp
  • chronark
  • perkinsjr

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Most modified files are formatting-only and relevant to the linked issue, but apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx appears to introduce substantive UI behavior (multi-select support, selection state, and new selection controls) that is unrelated to the number-formatting objective; a few other minor typing/parameter tweaks are present but low-risk. The keys-list functional additions are outside the stated scope of #3922 and increase review surface and risk for this PR. Because of that unrelated functional change, the PR contains out-of-scope modifications. Please remove or move the keys-list functional changes into a separate feature PR (or explicitly document and justify them in this PR and add tests), and keep this branch focused on formatting-only changes so reviewers can validate the number-formatting fix without unrelated functional churn.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix: Format pagination numbers in log footers" is concise and accurately describes the primary user-visible change to format pagination counts. It is clear, single-sentence, and signals a bugfix. The changeset applies formatting across multiple dashboard tables (APIs, projects, deployments, virtual table, etc.), so the title is slightly narrower than the full scope but still correct about the main fix.
Linked Issues Check ✅ Passed The changes add a locale-aware formatter (apps/dashboard/lib/fmt.ts -> formatNumberFull) and replace raw counts with formatted outputs in logs tables and the other listed components, which directly satisfies the linked issue #3922 request to use Intl.NumberFormat for "Showing X of Y" counts. Formatting has been applied across logs, virtual table indicator, API lists, projects, root keys, deployments, and authorization tables as described in the PR. This implements the core coding objective of the linked issue.
Description Check ✅ Passed The PR description follows the repository template, includes a clear summary, references the fixed issue (#3922), lists affected components, and provides an actionable "How should this be tested" section; this makes the description largely complete and reviewable. The Type of change and Checklist are present and many items are checked, improving traceability. However, some required checklist items remain unchecked (notably 'pnpm build' and 'pnpm fmt' as reported), which should be completed or explained in the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 15, 2025

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (1)

159-177: Deduplicate severity mapping; tighten types

categorizeSeverity duplicates getStatusType logic and accepts string, hiding typos at compile time. Consolidate to a single source of truth and type it as LogOutcomeType.

Apply:

-export const categorizeSeverity = (
-  outcome: string
-): keyof typeof STATUS_STYLES => {
+export const categorizeSeverity = (
+  outcome: LogOutcomeType
+): keyof typeof STATUS_STYLES => {
   switch (outcome) {
@@
-    default:
-      return "success";
+    default:
+      return "error";
   }
};

Then remove getStatusType and reuse categorizeSeverity where colors are computed:

- STATUS_STYLES[getStatusType(outcomeInfo.type)].badge.selected
+ STATUS_STYLES[categorizeSeverity(outcomeInfo.type)].badge!.selected
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)

337-337: Remove unsafe cast in rowClassName

Casting selectedKey to KeyDetails can hide null cases at compile time and risk runtime errors in helpers. Pass the nullable value and type helpers accordingly.

-        rowClassName={(log) => getRowClassName(log, selectedKey as KeyDetails)}
+        rowClassName={(log) => getRowClassName(log, selectedKey)}

If getRowClassName expects a non-null second arg, update its signature to accept KeyDetails | null.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10f390e and c8a35f0.

📒 Files selected for processing (13)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (15 hunks)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (11 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (3 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (1 hunks)
  • apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (11 hunks)
  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (6 hunks)
  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (9 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (9 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (3 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (7 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (10 hunks)
  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (14 hunks)
  • apps/dashboard/components/virtual-table/components/loading-indicator.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
📚 Learning: 2025-02-05T12:56:44.873Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2866
File: apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/log-details/components/log-footer.tsx:85-98
Timestamp: 2025-02-05T12:56:44.873Z
Learning: The RequestResponseDetails component in the ratelimit logs UI already handles empty content cases by preventing rendering when content is empty, so no additional empty state handling is needed in the parent components.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2025-09-12T17:57:12.128Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx:111-116
Timestamp: 2025-09-12T17:57:12.128Z
Learning: In the Deployment type used across the dashboard deployment components, the source field is required and will always be present, so optional chaining (source?.branch) is not needed when accessing source.branch or source.gitSha.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2024-12-03T14:21:19.543Z
Learnt from: chronark
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx:58-61
Timestamp: 2024-12-03T14:21:19.543Z
Learning: For the "Outcome" field in the `LogFooter` component (`apps/dashboard/app/(app)/logs/components/log-details/components/log-footer.tsx`), default to "N/A" instead of "VALID" when handling null values to avoid confusing customers.

Applied to files:

  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx
📚 Learning: 2025-08-29T12:08:57.482Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3880
File: apps/dashboard/app/(app)/settings/root-keys/components/root-key/root-key-dialog.tsx:137-141
Timestamp: 2025-08-29T12:08:57.482Z
Learning: Root keys in the Unkey system require at least one permission at minimum, so there is no valid case where a root key would have zero permissions. This means checking for permission existence when determining UI labels is unnecessary.

Applied to files:

  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx
📚 Learning: 2025-04-24T14:34:30.621Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3115
File: apps/dashboard/components/logs/checkbox/filters-popover.tsx:33-55
Timestamp: 2025-04-24T14:34:30.621Z
Learning: In the ShortcutActivator component within filters-popover.tsx, the purpose is to track keys separately for each filter item, providing a registration mechanism for shortcuts passed to it rather than enforcing specific key combinations like option+shift+key.

Applied to files:

  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
🧬 Code graph analysis (11)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (2)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts (1)
  • useRatelimitOverviewLogsQuery (15-129)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/utils/get-row-class.ts (2)
  • STATUS_STYLES (16-37)
  • getRowClassName (43-61)
apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (2)
apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts (1)
  • useRolesListQuery (8-82)
apps/dashboard/app/(app)/authorization/roles/components/table/components/selection-controls/index.tsx (1)
  • SelectionControls (14-104)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (5)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/skeletons.tsx (1)
  • ActionColumnSkeleton (78-89)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/hooks/use-deployments-list-query.ts (1)
  • useDeploymentsListQuery (11-123)
apps/dashboard/lib/shorten-id.ts (1)
  • shortenId (5-48)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/env-status-badge.tsx (1)
  • EnvStatusBadge (28-41)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/utils/get-row-class.ts (1)
  • getRowClassName (37-50)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (1)
apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx (1)
  • ProjectCard (21-111)
apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (5)
apps/dashboard/app/(app)/authorization/permissions/components/table/hooks/use-permissions-list-query.ts (1)
  • usePermissionsListQuery (11-86)
apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts (2)
  • Permission (19-19)
  • permissions (9-17)
internal/icons/src/icons/page-2.tsx (1)
  • Page2 (15-76)
apps/dashboard/app/(app)/authorization/permissions/components/table/utils/get-row-class.ts (1)
  • getRowClassName (26-38)
apps/dashboard/app/(app)/authorization/permissions/components/table/components/skeletons.tsx (4)
  • RoleColumnSkeleton (4-16)
  • SlugColumnSkeleton (18-25)
  • AssignedKeysColumnSkeleton (27-34)
  • AssignedToKeysColumnSkeleton (36-43)
apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (1)
apps/dashboard/app/(app)/apis/_components/controls/index.tsx (1)
  • ApiListControls (17-29)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (4)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/utils/get-row-class.ts (2)
  • categorizeSeverity (59-79)
  • STATUS_STYLES (16-57)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/context/logs.tsx (1)
  • useKeyDetailsLogsContext (31-37)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/hooks/use-logs-query.ts (1)
  • useKeyDetailsLogsQuery (22-236)
internal/clickhouse/src/verifications.ts (1)
  • KeyDetailsLog (66-66)
apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (5)
packages/rbac/src/permissions.ts (1)
  • UnkeyPermission (83-83)
internal/icons/src/icons/dots.tsx (1)
  • Dots (15-60)
apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts (1)
  • RootKey (35-35)
apps/dashboard/components/virtual-table/types.ts (1)
  • Column (13-24)
apps/dashboard/app/(app)/settings/root-keys/components/table/components/actions/root-keys-table-action.popover.constants.tsx (1)
  • RootKeysTableActions (12-15)
apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (2)
internal/clickhouse/src/logs.ts (2)
  • log (24-38)
  • Log (40-40)
apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts (1)
  • useLogsQuery (19-241)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts (1)
  • useRatelimitLogsQuery (17-208)
internal/clickhouse/src/logs.ts (1)
  • log (24-38)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)
internal/icons/src/icons/dots.tsx (1)
  • Dots (15-60)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/hooks/use-keys-list-query.ts (1)
  • useKeysListQuery (13-85)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value.tsx (1)
  • HiddenValueCell (5-47)
🔇 Additional comments (46)
apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (9)

16-18: Type guard looks good

isUnkeyPermission keeps downstream types clean. LGTM.


37-40: Dynamic import fallback polish LGTM

Lazy-loading actions + accessible loading button state read well.

Also applies to: 47-47, 49-52, 55-55


83-84: Correct dependency for memoized row class

Including selectedRootKey is correct to avoid stale closures.


115-117: Empty state copy tweak LGTM

Clearer guidance reads well.


145-146: Memoized config LGTM

Stable ref prevents VirtualTable re-renders.


153-159: Skeleton row spacing alignment LGTM

Consistent paddings improve visual rhythm.

Also applies to: 165-165, 177-178


281-283: TimestampInfo class tweak LGTM


306-307: Action column render LGTM


311-312: Columns memo deps LGTM

apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (3)

70-71: Selected background toggle LGTM


122-125: Selected-row highlighting LGTM

Also applies to: 136-139, 150-153, 164-167


259-260: Skeleton spacing LGTM

Also applies to: 266-268

apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (3)

32-34: No-op formatting change

Trailing comma removal is fine.


64-68: Controls props reflow LGTM


94-99: Button props reflow LGTM

apps/dashboard/app/(app)/projects/_components/list/index.tsx (1)

41-43: Empty state copy wrap LGTM

apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (5)

31-41: Query hook destructuring LGTM

Good readability and ordering.


76-81: Badge selected/default styling LGTM

Also applies to: 112-115


193-196: Pointer-events guard LGTM

Prevents unintended interactions on non-selected rows.


225-227: Row className delegation LGTM


250-253: Empty state copy wrap LGTM

apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (7)

19-26: UI imports consolidation LGTM


191-211: Query hook usage LGTM

Poll interval, startPolling wiring look correct.


213-228: Row className helper: good encapsulation

Keeps styling rules centralized.


260-260: Dependency array LGTM

Includes stable utils and timestamp refs.


278-281: Pointer-events guard LGTM


308-311: Badge color selection LGTM after dedup

Once categorizeSeverity is the single source, this remains clean.


349-356: Long-tag tooltip formatting LGTM

Readable truncation with copy affordance looks good.

Also applies to: 367-369, 415-421, 433-435

apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (5)

25-26: Query wiring LGTM


60-60: Selected background toggle LGTM


168-171: Header SelectionControls injection LGTM

Once IDs are used, bulk actions should align with useDeleteRole.


191-193: Empty state copy wrap LGTM


221-221: Skeleton spacing LGTM

apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (1)

46-47: Use shared formatter and make this a client component to avoid locale/hydration issues

Apply this diff in the shown file:

-          Showing {new Intl.NumberFormat().format(apiList.length)} of{" "}
-          {new Intl.NumberFormat().format(total)} APIs
+          Showing {formatNumber(apiList.length)} of {formatNumber(total)} APIs

Add the import near the top:

+import { formatNumber } from "@/lib/fmt";

If this component renders interactive controls or relies on client-side handlers, make it a client component:

+ "use client";

Verified occurrence: new Intl.NumberFormat(...).format found at lines 46–47.

apps/dashboard/components/virtual-table/components/loading-indicator.tsx (2)

66-68: LGTM: countInfoText wrapper improves consistency

Wrapping countInfoText is fine and non-breaking.


121-123: LGTM: conditional countInfoText block

No functional changes; readable structure.

apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (2)

144-147: Confirm cn supports array arguments

You’re passing an array of class names conditionally. Ensure cn (your wrapper) accepts arrays like clsx; otherwise, join into a string.

If arrays aren’t supported:

-        ) && ["opacity-50", "hover:opacity-100"],
+        ) && "opacity-50 hover:opacity-100",

165-168: Pointer-events disabled on non-selected timestamps — double-check UX

Disabling pointer events on other rows’ timestamps may block expected interactions (e.g., copying/opening). Confirm this is intentional.

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

31-33: Verify dynamic import path

Confirm "./components/actions/deployment-list-table-action.popover.constants" exists and exports DeploymentListTableActions as named. If not, this will fail at runtime.


317-319: LGTM: rowClassName extraction

Delegating to getRowClassName keeps table logic focused.

apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)

94-101: Confirm cn supports array arguments

Same pattern as main logs table; ensure cn handles arrays; otherwise use a space-joined string.

-        ) && ["opacity-50", "hover:opacity-100"],
+        ) && "opacity-50 hover:opacity-100",

118-121: Pointer-events disabled on non-selected timestamps — verify UX

This may hinder interactions on non-selected rows. Validate intent.

apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (4)

40-46: LGTM: loading fallback polish

Improved Dots styling is fine and non-functional.


100-116: LGTM: navigating state UX

Nice touch showing a spinner while navigating; prevents flicker.


223-227: LGTM: pass selected to HiddenValueCell

Prop will enable focused styling without side effects.


409-419: LGTM: skeletons for unspecified columns

Good UX improvement; safe and scoped.

Comment on lines +166 to 174
<span className="font-mono bg-gray-4 p-1 rounded">
{identity}
</span>
</Link>
) : (
<span className="font-mono bg-gray-4 p-1 rounded">{identity}</span>
<span className="font-mono bg-gray-4 p-1 rounded">
{identity}
</span>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Make the tooltip trigger focusable for keyboard users

iconContainer is a div; with asChild, the trigger won’t be focusable. Add tabIndex and an aria-label for a11y.

-                    {React.cloneElement(iconContainer, {
-                      className: cn(
-                        iconContainer.props.className,
-                        "cursor-pointer"
-                      ),
-                    })}
+                    {React.cloneElement(iconContainer, {
+                      className: cn(iconContainer.props.className, "cursor-pointer"),
+                      tabIndex: 0,
+                      role: "button",
+                      "aria-label": "Key identity info",
+                    })}

Also applies to: 179-184

🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
around lines 166-174 (and likewise 179-184), the tooltip trigger is using a div
asChild which is not keyboard-focusable; make the trigger accessible by either
replacing the div with a semantic focusable element (e.g., a button) or adding
tabIndex={0} and a descriptive aria-label to the existing element so it can
receive keyboard focus and convey its purpose to assistive tech; apply the same
change to the second occurrence at lines 179-184.

Comment on lines +352 to 359
<span>Showing</span>{" "}
<span className="text-accent-12">
{new Intl.NumberFormat().format(keys.length)}
</span>
<span>of</span>
{totalCount}
{new Intl.NumberFormat().format(totalCount)}
<span>keys</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reuse a memoized number formatter

Apply the shared formatter to keep consistency with the rest of the PR.

-              <span className="text-accent-12">
-                {new Intl.NumberFormat().format(keys.length)}
-              </span>
+              <span className="text-accent-12">{nf.format(keys.length)}</span>
...
-              {new Intl.NumberFormat().format(totalCount)}
+              {nf.format(totalCount)}

Support (outside selected lines):

import React, { useCallback, useMemo, useState } from "react";
const nf = useMemo(() => new Intl.NumberFormat(), []);
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
around lines 352 to 359, the code creates new Intl.NumberFormat instances inline
instead of reusing the shared memoized formatter; replace both occurrences of
new Intl.NumberFormat().format(...) with the existing nf.format(...) and ensure
nf is defined with useMemo (const nf = useMemo(() => new Intl.NumberFormat(),
[])) in the component scope so the shared, memoized formatter is used
consistently across the file.

Comment on lines 25 to 41
const {
permissions,
isLoading,
isLoadingMore,
loadMore,
totalCount,
hasMore,
} = usePermissionsListQuery();
const [selectedPermission, setSelectedPermission] =
useState<Permission | null>(null);
const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
new Set()
);
const [hoveredPermissionName, setHoveredPermissionName] = useState<
string | null
>(null);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Selection state should use stable IDs, not names

Using permission.name risks collisions and breaks bulk actions that typically expect IDs.

Apply:

-  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
-    new Set()
-  );
-  const [hoveredPermissionName, setHoveredPermissionName] = useState<
-    string | null
-  >(null);
+  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set());
+  const [hoveredPermissionId, setHoveredPermissionId] = useState<string | null>(null);
@@
-  const toggleSelection = useCallback((permissionName: string) => {
+  const toggleSelection = useCallback((permissionId: string) => {
     setSelectedPermissions((prevSelected) => {
       const newSelected = new Set(prevSelected);
-      if (newSelected.has(permissionName)) {
-        newSelected.delete(permissionName);
+      if (newSelected.has(permissionId)) {
+        newSelected.delete(permissionId);
       } else {
-        newSelected.add(permissionName);
+        newSelected.add(permissionId);
       }
       return newSelected;
     });
   }, []);

Propagate usages:

- const isSelected = selectedPermissions.has(permission.name);
- const isHovered = hoveredPermissionName === permission.name;
+ const isSelected = selectedPermissions.has(permission.permissionId);
+ const isHovered = hoveredPermissionId === permission.permissionId;
@@
- onMouseEnter={() => setHoveredPermissionName(permission.name)}
- onMouseLeave={() => setHoveredPermissionName(null)}
+ onMouseEnter={() => setHoveredPermissionId(permission.permissionId)}
+ onMouseLeave={() => setHoveredPermissionId(null)}
@@
- onCheckedChange={() => toggleSelection(permission.name)}
+ onCheckedChange={() => toggleSelection(permission.permissionId)}

Update memo deps accordingly:

-  [
-    selectedPermissions,
-    toggleSelection,
-    hoveredPermissionName,
-    selectedPermission?.permissionId,
-  ]
+  [selectedPermissions, toggleSelection, hoveredPermissionId, selectedPermission?.permissionId]

Confirm SelectionControls expects IDs. If not, align both ends.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== 1) Show target file if exists ==="
TARGET='apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx'
if [ -f "$TARGET" ]; then
  echo "FOUND: $TARGET"
  sed -n '1,320p' "$TARGET" || true
else
  echo "NOT FOUND: $TARGET"
  echo "Searching for files named permissions-list.tsx..."
  rg -n --hidden --files -g '!**/node_modules/**' 'permissions-list.tsx' || true
fi

echo
echo "=== 2) Find SelectionControls symbol occurrences ==="
rg -n --hidden -S '\bSelectionControls\b' -g '!**/node_modules/**' -C3 || true
echo
echo "=== 3) If SelectionControls files found, show their first matches ==="
rg --hidden -n --files -g '!**/node_modules/**' '\bSelectionControls\b' | head -n 10 || true
rg -n --hidden -S '\bSelectionControls\b' -g '!**/node_modules/**' --files | while read -r f; do
  echo "---- $f ----"
  sed -n '1,240p' "$f" || true
  echo
done

echo "=== 4) Search for onCheckedChange usages ==="
rg -n --hidden -S 'onCheckedChange' -g '!**/node_modules/**' -C3 || true

echo "=== 5) Search for selectedPermissions variable usages ==="
rg -n --hidden -S '\bselectedPermissions\b' -g '!**/node_modules/**' -C3 || true

echo "=== 6) Search for hoveredPermissionName / hoveredPermissionId usages ==="
rg -n --hidden -S '\bhoveredPermissionName\b' -g '!**/node_modules/**' -C3 || true
rg -n --hidden -S '\bhoveredPermissionId\b' -g '!**/node_modules/**' -C3 || true

echo "=== 7) Search for Permission type/interface definitions ==="
rg -n --hidden -S '\binterface\s+Permission\b' -g '!**/node_modules/**' -C3 || true
rg -n --hidden -S '\btype\s+Permission\b' -g '!**/node_modules/**' -C3 || true
rg -n --hidden -S '\bexport\s+type\s+Permission\b' -g '!**/node_modules/**' -C3 || true

echo "=== 8) Search for permission.permissionId / permission.id / permission.name ==="
rg -n --hidden -S 'permission\.permissionId' -g '!**/node_modules/**' -C3 || true
rg -n --hidden -S 'permission\.id\b' -g '!**/node_modules/**' -C3 || true
rg -n --hidden -S 'permission\.name\b' -g '!**/node_modules/**' -C3 || true

echo "=== 9) Search for toggleSelection function/usages ==="
rg -n --hidden -S '\btoggleSelection\b' -g '!**/node_modules/**' -C5 || true

echo "=== 10) Search for SelectionControls import in the target file ==="
if [ -f "$TARGET" ]; then
  rg -n --hidden -S 'SelectionControls' "$TARGET" -C3 || true
fi

echo "=== done ==="

Length of output: 9648


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
COMP_DIR='apps/dashboard/app/(app)/authorization/permissions/components/table/components'
echo
echo "=== Listing components dir ==="
if [ -d "$COMP_DIR" ]; then
  ls -la "$COMP_DIR" || true
else
  echo "Directory not found: $COMP_DIR"
fi

echo
echo "=== Try known selection-controls paths ==="
paths=(
  "$COMP_DIR/selection-controls.tsx"
  "$COMP_DIR/selection-controls.ts"
  "$COMP_DIR/selection-controls.jsx"
  "$COMP_DIR/selection-controls.js"
  'apps/dashboard/app/(app)/authorization/permissions/components/selection-controls.tsx'
  'apps/dashboard/app/(app)/authorization/permissions/components/selection-controls.ts'
)
for p in "${paths[@]}"; do
  if [ -f "$p" ]; then
    echo "FOUND: $p"
    sed -n '1,240p' "$p" || true
  else
    echo "Not found: $p"
  fi
done

echo
echo "=== Find any file names containing selection-controls ==="
find apps -type f -iname '*selection-controls*' -maxdepth 6 -print -exec sed -n '1,240p' {} \; || true

echo
echo "=== Grep for 'SelectionControls' symbol (fallback) ==="
grep -RIn --exclude-dir=node_modules --exclude-dir=.git "SelectionControls" apps || true

echo "=== done ==="

Length of output: 3406


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILES=(
  'apps/dashboard/app/(app)/authorization/permissions/components/table/components/selection-controls/index.tsx'
  'apps/dashboard/app/(app)/authorization/roles/components/table/components/selection-controls/index.tsx'
  'apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/selection-controls/index.tsx'
)

for f in "${FILES[@]}"; do
  echo "=== FILE: $f ==="
  if [ -f "$f" ]; then
    sed -n '1,240p' "$f" || true
  else
    echo "NOT FOUND: $f"
  fi
  echo
done

Length of output: 15089


Use stable IDs for selection state (permission.permissionId) — critical fix required

SelectionControls expects permission IDs (it passes permissionIds to the delete mutation), but PermissionsList currently toggles/checks by permission.name — this breaks bulk actions. Change selection/hover state and toggle to use permission.permissionId.

-  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
-    new Set()
-  );
-  const [hoveredPermissionName, setHoveredPermissionName] = useState<
-    string | null
-  >(null);
+  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set());
+  const [hoveredPermissionId, setHoveredPermissionId] = useState<string | null>(null);
@@
-  const toggleSelection = useCallback((permissionName: string) => {
+  const toggleSelection = useCallback((permissionId: string) => {
     setSelectedPermissions((prevSelected) => {
       const newSelected = new Set(prevSelected);
-      if (newSelected.has(permissionName)) {
-        newSelected.delete(permissionName);
+      if (newSelected.has(permissionId)) {
+        newSelected.delete(permissionId);
       } else {
-        newSelected.add(permissionName);
+        newSelected.add(permissionId);
       }
       return newSelected;
     });
   }, []);

Propagate usages:

- const isSelected = selectedPermissions.has(permission.name);
- const isHovered = hoveredPermissionName === permission.name;
+ const isSelected = selectedPermissions.has(permission.permissionId);
+ const isHovered = hoveredPermissionId === permission.permissionId;
@@
- onMouseEnter={() => setHoveredPermissionName(permission.name)}
- onMouseLeave={() => setHoveredPermissionName(null)}
+ onMouseEnter={() => setHoveredPermissionId(permission.permissionId)}
+ onMouseLeave={() => setHoveredPermissionId(null)}
@@
- onCheckedChange={() => toggleSelection(permission.name)}
+ onCheckedChange={() => toggleSelection(permission.permissionId)}

Update memo deps accordingly:

-  [
-    selectedPermissions,
-    toggleSelection,
-    hoveredPermissionName,
-    selectedPermission?.permissionId,
-  ]
+  [selectedPermissions, toggleSelection, hoveredPermissionId, selectedPermission?.permissionId]

File: apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx — apply changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {
permissions,
isLoading,
isLoadingMore,
loadMore,
totalCount,
hasMore,
} = usePermissionsListQuery();
const [selectedPermission, setSelectedPermission] =
useState<Permission | null>(null);
const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
new Set()
);
const [hoveredPermissionName, setHoveredPermissionName] = useState<
string | null
>(null);
const {
permissions,
isLoading,
isLoadingMore,
loadMore,
totalCount,
hasMore,
} = usePermissionsListQuery();
const [selectedPermission, setSelectedPermission] =
useState<Permission | null>(null);
const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set());
const [hoveredPermissionId, setHoveredPermissionId] = useState<string | null>(null);
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx
around lines 25–41, the selection and hover state currently use permission.name
but SelectionControls and delete mutation require stable IDs; change state and
logic to use permission.permissionId instead: replace selectedPermissions
Set<string> that stores names with one that stores permission.permissionId,
rename hoveredPermissionName to hoveredPermissionId (string | null) and update
all toggle/check handlers, checkbox checked conditions, and any calls that pass
selectedPermissions to use these IDs; update memo dependencies to reference
permission.permissionId where appropriate and ensure SelectionControls receives
the permissionIds set (not names).

Comment on lines +235 to 241
<span>Showing</span>{" "}
<span className="text-accent-12">
{new Intl.NumberFormat().format(historicalLogs.length)}
</span>
<span>of {new Intl.NumberFormat().format(totalCount)}</span>
<span>rate limit identifiers</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use shared formatter for footer counts

You already import formatNumber; reuse it here too.

-            <span>Showing</span>{" "}
-            <span className="text-accent-12">
-              {new Intl.NumberFormat().format(historicalLogs.length)}
-            </span>
-            <span>of {new Intl.NumberFormat().format(totalCount)}</span>
+            <span>Showing</span> <span className="text-accent-12">{formatNumber(historicalLogs.length)}</span>
+            <span>of {formatNumber(totalCount)}</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span>Showing</span>{" "}
<span className="text-accent-12">
{new Intl.NumberFormat().format(historicalLogs.length)}
</span>
<span>of {new Intl.NumberFormat().format(totalCount)}</span>
<span>rate limit identifiers</span>
</div>
<span>Showing</span> <span className="text-accent-12">{formatNumber(historicalLogs.length)}</span>
<span>of {formatNumber(totalCount)}</span>
<span>rate limit identifiers</span>
</div>
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
around lines 235 to 241, the footer uses new Intl.NumberFormat().format(...) for
counts; replace both usages with the already-imported formatNumber(...) helper
so the component uses the shared formatter consistently, e.g. call
formatNumber(historicalLogs.length) and formatNumber(totalCount) in those spans.

Comment on lines +238 to 245
<span>Showing</span>{" "}
<span className="text-accent-12">
{new Intl.NumberFormat().format(historicalLogs.length)}
</span>
<span>of</span>
{totalCount}
{new Intl.NumberFormat().format(totalCount)}
<span>ratelimit requests</span>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reuse a memoized number formatter

Apply the shared formatter here too.

-            <span className="text-accent-12">
-              {new Intl.NumberFormat().format(historicalLogs.length)}
-            </span>
+            <span className="text-accent-12">{nf.format(historicalLogs.length)}</span>
...
-            {new Intl.NumberFormat().format(totalCount)}
+            {nf.format(totalCount)}

Support (outside selected lines):

import { useMemo } from "react";
const nf = useMemo(() => new Intl.NumberFormat(), []);
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
around lines 238 to 245, the code creates new Intl.NumberFormat() inline twice;
replace those inline constructors with a shared, memoized formatter (e.g.,
define const nf = useMemo(() => new Intl.NumberFormat(), [] ) at the top of the
component) and then use nf.format(...) for both historicalLogs.length and
totalCount; ensure useMemo is imported from react and that nf is declared inside
the component so it is reused across renders.

Comment on lines 189 to 191
.map((p) => p.name)
.filter(isUnkeyPermission);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Permission normalization: optional simplification

Given prior learning that root keys always have ≥1 permission, the nullish coalescing could be dropped. Harmless as-is; consider simplifying.

Based on retrieved learnings from PR #3880, confirm if zero-permission root keys are impossible in current API responses.

🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx
around lines 189 to 191, the code currently uses a nullish-coalescing fallback
for permissions; remove the `?? []` fallback so you call
.map(...).filter(isUnkeyPermission) directly since root keys are guaranteed to
have ≥1 permission. Before committing, verify the API contract (PR
unkeyed/unkey#3880) to confirm zero-permission root keys cannot be returned; if
verification fails, keep the defensive fallback or add an explicit runtime
assertion and error handling.

@perkinsjr
Copy link
Member

Hi @naaa760 Please run pnpm fmt from the root of the Unkey monorepo.

@naaa760
Copy link
Author

naaa760 commented Sep 15, 2025

@perkinsjr
done it!
I have run pnpm fmt and the code is properly formatted

- Add formatNumberFull utility function to /lib/fmt.ts for consistent number formatting
- Replace all inline new Intl.NumberFormat().format() calls with formatNumberFull()
- Apply formatting to pagination footers across all log tables and lists
- Numbers now display as 1,239,436 instead of 1239436 for better readability
- Maintains consistency with existing utility pattern in codebase
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (1)

36-54: Critical: selection/hover state must use permission.permissionId (fix required)

selectedPermissions, hover state, and toggleSelection currently use permission.name — switch them to permission.permissionId. Name-based checks were found in apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (lines ~63–64); update state, handlers, render checks, event handlers, and dependency arrays, then scan the file for any remaining permission.name uses.

-  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(
-    new Set()
-  );
-  const [hoveredPermissionName, setHoveredPermissionName] = useState<
-    string | null
-  >(null);
+  const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set());
+  const [hoveredPermissionId, setHoveredPermissionId] = useState<string | null>(null);

-  const toggleSelection = useCallback((permissionName: string) => {
+  const toggleSelection = useCallback((permissionId: string) => {
     setSelectedPermissions((prevSelected) => {
       const newSelected = new Set(prevSelected);
       if (newSelected.has(permissionName)) {
         newSelected.delete(permissionName);
       } else {
         newSelected.add(permissionName);
       }
       return newSelected;
     });
   }, []);
@@
-          const isSelected = selectedPermissions.has(permission.name);
-          const isHovered = hoveredPermissionName === permission.name;
+          const isSelected = selectedPermissions.has(permission.permissionId);
+          const isHovered = hoveredPermissionId === permission.permissionId;
@@
-              onMouseEnter={() => setHoveredPermissionName(permission.name)}
-              onMouseLeave={() => setHoveredPermissionName(null)}
+              onMouseEnter={() => setHoveredPermissionId(permission.permissionId)}
+              onMouseLeave={() => setHoveredPermissionId(null)}
@@
-                  onCheckedChange={() => toggleSelection(permission.name)}
+                  onCheckedChange={() => toggleSelection(permission.permissionId)}
@@
-    [
-      selectedPermissions,
-      toggleSelection,
-      hoveredPermissionName,
-      selectedPermission?.permissionId,
-    ]
+    [selectedPermissions, toggleSelection, hoveredPermissionId, selectedPermission?.permissionId]
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)

169-179: Duration treats 0 as empty; confirm units (ms vs µs)
A duration of 0 currently renders “”. Also verify the unit is actually milliseconds.

Apply this diff:

-            <div className="font-mono">
-              {parsedDuration ? msToSeconds(parsedDuration) : "<EMPTY>"}
-            </div>
+            <div className="font-mono">
+              {parsedDuration != null ? msToSeconds(parsedDuration) : "<EMPTY>"}
+            </div>

186-201: Reset column: double JSON parse, inconsistent empty label, and falsy‑0 bug

  • Parses response_body twice.
  • Uses "<Empty>" vs "<EMPTY>" elsewhere.
  • Falsy check hides valid 0 timestamps.

Apply this diff:

-        render: (log) => {
-          const parsedReset = safeParseJson(log.response_body)?.reset;
-          return parsedReset ? (
-            <div className="font-mono">
-              <TimestampInfo
-                value={safeParseJson(log.response_body)?.reset ?? "<EMPTY>"
-                }
-                className={cn(
-                  "font-mono group-hover:underline decoration-dotted",
-                  selectedLog &&
-                    selectedLog.request_id !== log.request_id &&
-                    "pointer-events-none"
-                )}
-              />
-            </div>
-          ) : (
-            "<Empty>"
-          );
-        },
+        render: (log) => {
+          const body = safeParseJson(log.response_body);
+          const parsedReset = body?.reset;
+          return parsedReset != null ? (
+            <div className="font-mono">
+              <TimestampInfo
+                value={parsedReset}
+                className={cn(
+                  "font-mono group-hover:underline decoration-dotted",
+                  selectedLog &&
+                    selectedLog.request_id !== log.request_id &&
+                    "pointer-events-none"
+                )}
+              />
+            </div>
+          ) : (
+            "<EMPTY>"
+          );
+        },
♻️ Duplicate comments (8)
apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (1)

52-73: Critical: selection uses role.name — switch to role.roleId

Use stable IDs to avoid collisions and to match bulk actions.

-  const [selectedRoles, setSelectedRoles] = useState<Set<string>>(new Set());
-  const [hoveredRoleName, setHoveredRoleName] = useState<string | null>(null);
+  const [selectedRoles, setSelectedRoles] = useState<Set<string>>(new Set());
+  const [hoveredRoleId, setHoveredRoleId] = useState<string | null>(null);
@@
-          const isSelected = selectedRoles.has(role.name);
-          const isHovered = hoveredRoleName === role.name;
+          const isSelected = selectedRoles.has(role.roleId);
+          const isHovered = hoveredRoleId === role.roleId;
@@
-              onMouseEnter={() => setHoveredRoleName(role.name)}
-              onMouseLeave={() => setHoveredRoleName(null)}
+              onMouseEnter={() => setHoveredRoleId(role.roleId)}
+              onMouseLeave={() => setHoveredRoleId(null)}
@@
-                  onCheckedChange={() => toggleSelection(role.name)}
+                  onCheckedChange={() => toggleSelection(role.roleId)}
@@
-    [selectedRoles, toggleSelection, hoveredRoleName, selectedRole?.roleId]
+    [selectedRoles, toggleSelection, hoveredRoleId, selectedRole?.roleId]

Also change toggleSelection to accept roleId (string) and update its internals accordingly.

#!/bin/bash
rg -nP --hidden -C2 '\bselectedRoles\.has\(\s*role\.name' apps/dashboard/app/\(app\)/authorization/roles

Also applies to: 150-151

apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (1)

188-191: Root key permissions: drop unnecessary fallback (subject to contract)

Per prior learning, root keys always have ≥1 permission; the ?? [] and validation may be redundant. If API contract guarantees this, simplify; otherwise keep with an assertion.

-const permissions = editingKey.permissions ?? [];
-const validatedPermissions = permissions
-  .map((p) => p.name)
-  .filter(isUnkeyPermission);
+const validatedPermissions = editingKey.permissions
+  .map((p) => p.name)
+  .filter(isUnkeyPermission);

Context: retrieved learnings indicate zero-permission root keys are invalid. Please confirm current API responses.

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (1)

159-161: Hardcoded “2 CPU” — bind to data

Earlier feedback still applies. If CPU varies, read from the deployment object; otherwise annotate why it’s constant.

-                        <span className="font-semibold text-grayA-12 tabular-nums">
-                          2
-                        </span>
+                        <span className="font-semibold text-grayA-12 tabular-nums">
+                          {deployment.cpu ?? 2 /* fallback if truly absent */}
+                        </span>
apps/dashboard/app/(app)/projects/_components/list/index.tsx (1)

81-83: Date formatting consistency

Prefer a shared date formatter to avoid env-locale variance and ensure consistent timezone/format.

Example:

// lib/date.ts
export const formatDate = (ts: number | string | Date, locale = "en-US") =>
  new Intl.DateTimeFormat(locale, { year: "numeric", month: "short", day: "2-digit" }).format(new Date(ts));
-commitDate={new Date(project.updatedAt || project.createdAt).toLocaleDateString()}
+commitDate={formatDate(project.updatedAt || project.createdAt)}
apps/dashboard/components/virtual-table/components/loading-indicator.tsx (1)

1-1: Consider using a memoized formatter to avoid re-instantiation.

The formatNumberFull function internally creates a new Intl.NumberFormat instance on each call. For better performance, especially during re-renders, consider memoizing the formatter instance.

+import { useMemo } from "react";
 import { formatNumberFull } from "@/lib/fmt";

Then in the component:

+const formatNumber = useMemo(() => {
+  const formatter = new Intl.NumberFormat("en-US");
+  return (n: number) => formatter.format(n);
+}, []);

And use it as:

-          title={`${buttonText} • ${formatNumberFull(
-            totalVisible
-          )} of ${formatNumberFull(totalCount)} ${itemLabel}`}
+          title={`${buttonText} • ${formatNumber(totalVisible)} of ${formatNumber(totalCount)} ${itemLabel}`}

Also applies to: 61-63, 129-133

apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (1)

188-190: Optimize repeated function calls.

The extractResponseField function is called twice with identical parameters. Store the result to avoid redundant computation.

-              {extractResponseField(log, "code")
-                ? `| ${extractResponseField(log, "code")}`
-                : ""}
+              {(() => {
+                const code = extractResponseField(log, "code");
+                return code ? `| ${code}` : "";
+              })()}
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)

167-174: Add keyboard accessibility to the tooltip trigger.

The identity container tooltip trigger is not keyboard accessible. Add proper attributes to support keyboard navigation.

                    {React.cloneElement(iconContainer, {
                      className: cn(
                        iconContainer.props.className,
                        "cursor-pointer"
                      ),
+                     tabIndex: 0,
+                     role: "button",
+                     "aria-label": "View identity information",
                    })}
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (1)

238-245: Memoize and reuse a formatter; add SSR hydration guard on dynamic text

Replace per-call formatting with a shared formatter and prevent noisy hydration warnings if server/client locales differ.

Apply this diff:

-          <div className="flex gap-2">
+          <div className="flex gap-2" suppressHydrationWarning>
             <span>Showing</span>{" "}
             <span className="text-accent-12">
-              {formatNumberFull(historicalLogs.length)}
+              {nf.format(historicalLogs.length)}
             </span>
             <span>of</span>
-            {formatNumberFull(totalCount)}
+            {nf.format(totalCount)}
             <span>ratelimit requests</span>
           </div>

Support (outside selected lines, inside the component):

const nf = useMemo(() => new Intl.NumberFormat(), []);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a35f0 and 0769a91.

📒 Files selected for processing (14)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (16 hunks)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (12 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (4 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (2 hunks)
  • apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (12 hunks)
  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (7 hunks)
  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (10 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (10 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (4 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (8 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (11 hunks)
  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (15 hunks)
  • apps/dashboard/components/virtual-table/components/loading-indicator.tsx (3 hunks)
  • apps/dashboard/lib/fmt.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2024-10-08T15:33:04.290Z
Learnt from: AkshayBandi027
PR: unkeyed/unkey#2215
File: apps/dashboard/app/(app)/@breadcrumb/authorization/roles/[roleId]/page.tsx:28-29
Timestamp: 2024-10-08T15:33:04.290Z
Learning: In `authorization/roles/[roleId]/update-role.tsx`, the tag `role-${role.id}` is revalidated after updating a role to ensure that the caching mechanism is properly handled for roles.

Applied to files:

  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
📚 Learning: 2025-06-19T11:48:05.070Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3324
File: apps/dashboard/app/(app)/authorization/roles/components/table/components/actions/keys-table-action.popover.constants.tsx:17-18
Timestamp: 2025-06-19T11:48:05.070Z
Learning: In the authorization roles refactor, the RoleBasic type uses `roleId` as the property name for the role identifier, not `id`. This is consistent throughout the codebase in apps/dashboard/lib/trpc/routers/authorization/roles/query.ts.

Applied to files:

  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2025-09-12T17:57:12.128Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx:111-116
Timestamp: 2025-09-12T17:57:12.128Z
Learning: In the Deployment type used across the dashboard deployment components, the source field is required and will always be present, so optional chaining (source?.branch) is not needed when accessing source.branch or source.gitSha.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
📚 Learning: 2025-02-05T12:56:44.873Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2866
File: apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/log-details/components/log-footer.tsx:85-98
Timestamp: 2025-02-05T12:56:44.873Z
Learning: The RequestResponseDetails component in the ratelimit logs UI already handles empty content cases by preventing rendering when content is empty, so no additional empty state handling is needed in the parent components.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2025-08-29T12:08:57.482Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3880
File: apps/dashboard/app/(app)/settings/root-keys/components/root-key/root-key-dialog.tsx:137-141
Timestamp: 2025-08-29T12:08:57.482Z
Learning: Root keys in the Unkey system require at least one permission at minimum, so there is no valid case where a root key would have zero permissions. This means checking for permission existence when determining UI labels is unnecessary.

Applied to files:

  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx
📚 Learning: 2025-04-24T14:34:30.621Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3115
File: apps/dashboard/components/logs/checkbox/filters-popover.tsx:33-55
Timestamp: 2025-04-24T14:34:30.621Z
Learning: In the ShortcutActivator component within filters-popover.tsx, the purpose is to track keys separately for each filter item, providing a registration mechanism for shortcuts passed to it rather than enforcing specific key combinations like option+shift+key.

Applied to files:

  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
🧬 Code graph analysis (13)
apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (3)
apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts (1)
  • useRolesListQuery (8-82)
apps/dashboard/app/(app)/authorization/roles/components/table/components/selection-controls/index.tsx (1)
  • SelectionControls (14-104)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (1)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (2)
apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx (1)
  • ProjectCard (21-111)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (6)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/skeletons.tsx (1)
  • ActionColumnSkeleton (78-89)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/hooks/use-deployments-list-query.ts (1)
  • useDeploymentsListQuery (11-123)
apps/dashboard/lib/shorten-id.ts (1)
  • shortenId (5-48)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/env-status-badge.tsx (1)
  • EnvStatusBadge (28-41)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/utils/get-row-class.ts (1)
  • getRowClassName (37-50)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/components/virtual-table/components/loading-indicator.tsx (1)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (2)
apps/dashboard/app/(app)/apis/_components/controls/index.tsx (1)
  • ApiListControls (17-29)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (3)
internal/clickhouse/src/logs.ts (2)
  • log (24-38)
  • Log (40-40)
apps/dashboard/app/(app)/logs/components/table/hooks/use-logs-query.ts (1)
  • useLogsQuery (19-241)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (3)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts (1)
  • useRatelimitOverviewLogsQuery (15-129)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/utils/get-row-class.ts (2)
  • STATUS_STYLES (16-37)
  • getRowClassName (43-61)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (5)
packages/rbac/src/permissions.ts (1)
  • UnkeyPermission (83-83)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/lib/trpc/routers/settings/root-keys/query.ts (1)
  • RootKey (35-35)
apps/dashboard/components/virtual-table/types.ts (1)
  • Column (13-24)
apps/dashboard/app/(app)/settings/root-keys/components/table/components/actions/root-keys-table-action.popover.constants.tsx (1)
  • RootKeysTableActions (12-15)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (3)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/hooks/use-logs-query.ts (1)
  • useKeyDetailsLogsQuery (22-236)
internal/clickhouse/src/verifications.ts (1)
  • KeyDetailsLog (66-66)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (2)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/hooks/use-keys-list-query.ts (1)
  • useKeysListQuery (13-85)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (4)
apps/dashboard/app/(app)/authorization/permissions/components/table/hooks/use-permissions-list-query.ts (1)
  • usePermissionsListQuery (11-86)
apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts (2)
  • Permission (19-19)
  • permissions (9-17)
apps/dashboard/app/(app)/authorization/permissions/components/table/utils/get-row-class.ts (1)
  • getRowClassName (26-38)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts (1)
  • useRatelimitLogsQuery (17-208)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
🔇 Additional comments (20)
apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (2)

47-49: LGTM — counts now consistently formatted

Usage of formatNumberFull for both visible and total counts looks correct.


2-2: Import alignment

Centralizing number formatting via "@/lib/fmt" is consistent with prior guidance. Good change.

apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (3)

95-101: LGTM — footer count formatting

Correct adoption of formatNumberFull for “Showing X of Y root keys”.


17-21: LGTM — type guard

Good use of a narrow type guard using zod’s safeParse.


38-41: Dynamic import placeholder UX — LGTM

Good lightweight skeleton for actions popover with accessible focus styles.

Also applies to: 47-53, 56-56

apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (1)

214-220: LGTM — footer number formatting

Counts use formatNumberFull consistently.

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

327-333: LGTM — footer number formatting

Correct usage of formatNumberFull for deployments list.


204-216: LGTM — source fields without optional chaining

Matches learning that source is required; clean and safe.

apps/dashboard/app/(app)/projects/_components/list/index.tsx (1)

118-124: LGTM — header count formatting

Viewing X of Y now uses formatNumberFull consistently.

apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (1)

89-91: LGTM — footer number formatting

Switched to formatNumberFull for both shown and total API counts.

apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (1)

176-182: LGTM — footer number formatting

Counts use formatNumberFull for roles list.

apps/dashboard/lib/fmt.ts (1)

5-7: Make number formatting locale-aware

File: apps/dashboard/lib/fmt.ts (lines 5-7)

Hardcoded "en-US" contradicts "locale-appropriate" — accept an optional locale param (default "en-US") and confirm product decision (fixed en-US vs per-user locale) before changing callsites.

-export function formatNumberFull(n: number): string {
-  return Intl.NumberFormat("en-US").format(n);
-}
+export function formatNumberFull(n: number, locale?: string): string {
+  return Intl.NumberFormat(locale ?? "en-US").format(n);
+}
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (1)

493-499: LGTM! Number formatting correctly implemented.

The implementation properly formats the pagination counts using formatNumberFull, which addresses the requirements from issue #3922 to display large numbers with locale-appropriate separators.

apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (1)

268-274: LGTM! Pagination formatting correctly applied.

The pagination footer now properly formats numbers using formatNumberFull, making large counts more readable as per the PR objectives.

apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (1)

235-240: LGTM! Consistent number formatting applied.

The pagination footer correctly uses formatNumberFull for displaying counts, maintaining consistency with other tables in the application.

apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (2)

353-359: LGTM! Number formatting properly implemented.

The pagination footer correctly formats the counts using formatNumberFull, ensuring consistency across all table components and addressing the PR objectives.


66-89: Well-structured multi-select implementation.

The addition of multi-select functionality with checkbox controls is cleanly implemented with proper state management and callbacks. The UI gracefully shows checkboxes on hover and selection states.

apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (3)

66-81: Hook restructuring LGTM

Destructuring and parameterization of useRatelimitLogsQuery looks clean and non‑breaking.


157-167: Limit column change LGTM

Layout wrapper added; behavior preserved.


6-6: Hard‑coded en‑US formatter — make formatNumberFull locale‑aware or confirm US default

apps/dashboard/lib/fmt.ts currently calls Intl.NumberFormat("en-US") and is imported across the dashboard, so this is a global behavioral change. Replace fmt.ts with a locale‑aware, memoized formatter (or explicitly keep en‑US and update the PR title to “consistent US number formatting”). Proposed change (apply to apps/dashboard/lib/fmt.ts):

const nfCache = new Map<string, Intl.NumberFormat>();

function resolveLocale(explicit?: string) {
  if (explicit) return explicit;
  try {
    // Stable across SSR/CSR when your app sets a single locale
    return Intl.NumberFormat().resolvedOptions().locale;
  } catch {
    return "en-US";
  }
}

export function getNumberFormatter(
  locale?: string,
  options?: Intl.NumberFormatOptions,
) {
  const key = JSON.stringify([resolveLocale(locale), options ?? null]);
  let nf = nfCache.get(key);
  if (!nf) {
    nf = new Intl.NumberFormat(resolveLocale(locale), options);
    nfCache.set(key, nf);
  }
  return nf;
}

export function formatNumberFull(n: number, locale?: string): string {
  return getNumberFormatter(locale).format(n);
}

Comment on lines +95 to +97
!realtimeLogs.some(
(realtime) => realtime.request_id === log.request_id
) && ["opacity-50", "hover:opacity-100"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Avoid O(N) scan per row; precompute a Set of realtime IDs

realtimeLogs.some(...) runs for every row render. Use a Set for O(1) membership checks.

Apply this diff:

-        !realtimeLogs.some(
-          (realtime) => realtime.request_id === log.request_id
-        ) && ["opacity-50", "hover:opacity-100"],
+        !realtimeIds.has(log.request_id) && ["opacity-50", "hover:opacity-100"],

Support (outside selected lines):

const realtimeIds = useMemo(
  () => new Set(realtimeLogs.map((r) => r.request_id)),
  [realtimeLogs],
);
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
around lines 95 to 97, currently each row runs realtimeLogs.some(...) causing an
O(N) scan per row; precompute a Set of realtime request IDs with useMemo (depend
on realtimeLogs) and replace the per-row .some(...) check with a constant-time
Set.has(log.request_id) lookup so rendering is O(1) per row.

Comment on lines 1 to 3
export function formatNumber(n: number): string {
return Intl.NumberFormat("en", { notation: "compact" }).format(n);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consistency: unify locales and avoid per-call allocations (optional)

formatNumber uses "en" while formatNumberFull uses "en-US". Consider aligning both and optionally caching formatters at module scope to avoid repeated Intl allocations. Non-blocking.

🤖 Prompt for AI Agents
In apps/dashboard/lib/fmt.ts around lines 1 to 3, formatNumber currently uses
the locale "en" which is inconsistent with formatNumberFull's "en-US" and it
creates a new Intl.NumberFormat on every call; change the locale to "en-US" to
match the other function and move Intl.NumberFormat instances to module scope
(create and reuse a cached compact formatter and, if needed, a full formatter)
so the exported formatNumber simply calls the pre-created formatter.format(n) to
avoid per-call allocations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (1)

132-136: CPU value is blank — wire up the actual field

The "Size" column renders an empty CPU value. This regresses the earlier “hardcoded 2 CPU” issue; now it shows nothing. Wire it to the runtime config CPU field (with a safe fallback).

Apply:

-                        <span className="font-semibold text-grayA-12 tabular-nums">
-
-                        </span>
+                        <span className="font-semibold text-grayA-12 tabular-nums">
+                          {deployment.runtimeConfig.cpu ?? "—"}
+                        </span>

If the field name differs (e.g., vcpu/cpus/cores), adjust accordingly. To confirm the correct field:

#!/usr/bin/env bash
# Find CPU-like fields on runtimeConfig to wire up correctly
rg -nP --type=ts '(interface|type)\s+.*RuntimeConfig|runtimeConfig\.(cpu|cpus|vcpu|vcpus|cores|memory)' -C2
🧹 Nitpick comments (2)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

111-115: Optional: format large numbers with the shared formatter

For consistency with this PR’s objective, format the VM count and memory with the repo’s number formatter.

+import { formatNumberFull } from "@/lib/fmt";
...
-                        {deployment.runtimeConfig.regions.reduce(
-                          (acc, region) => acc + region.vmCount,
-                          0,
-                        )}
+                        {formatNumberFull(
+                          deployment.runtimeConfig.regions.reduce(
+                            (acc, region) => acc + region.vmCount,
+                            0,
+                          ),
+                        )}
...
-                          {deployment.runtimeConfig.memory}
+                          {formatNumberFull(deployment.runtimeConfig.memory)}

Also applies to: 140-141


283-283: Tighten useMemo deps to avoid unnecessary recompute

Depending on your data source, tracking whole query objects can churn the memo. Prefer stable slices.

-  }, [selectedDeployment?.deployment.id, isCompactView, deployments, activeDeployment]);
+  }, [
+    selectedDeployment?.deployment.id,
+    isCompactView,
+    deployments.data,
+    activeDeployment.data,
+  ]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0769a91 and 470bc45.

📒 Files selected for processing (2)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (9 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/skeletons.tsx (1)
  • ActionColumnSkeleton (78-89)
apps/dashboard/lib/shorten-id.ts (1)
  • shortenId (5-54)
🔇 Additional comments (2)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

33-35: LGTM: import formatting, selected styling, accent text, timestamp wrapper, and empty-state copy

Changes look good and align with existing patterns.

Also applies to: 39-39, 61-61, 76-76, 163-163, 177-177, 213-217, 300-301


5-5: Whitespace-only changes

No action needed.

Also applies to: 81-81, 293-293

@@ -407,15 +290,15 @@ export const DeploymentsList = ({ projectId }: Props) => {
onRowClick={setSelectedDeployment}
selectedItem={selectedDeployment}
keyExtractor={(deployment) => deployment.id}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix keyExtractor to use row.deployment.id

VirtualTable rows appear to be shaped as { deployment, environment }. The current extractor uses the row itself as a Deployment, which likely produces undefined/duplicate keys.

-      keyExtractor={(deployment) => deployment.id}
+      keyExtractor={(row) => row.deployment.id}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
keyExtractor={(deployment) => deployment.id}
keyExtractor={(row) => row.deployment.id}
🤖 Prompt for AI Agents
In
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
around line 292, the keyExtractor currently treats the row as a Deployment and
returns deployment.id from the row itself, but rows are shaped as { deployment,
environment } so this yields undefined/duplicate keys; update the keyExtractor
to return row.deployment.id (or otherwise access the nested deployment object)
to ensure a stable, unique key for each VirtualTable row.

@ogzhanolguncu
Copy link
Contributor

Can you please go through coderabbit comments and fix the conflicts?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (1)

15-21: Add missing imports — import collection & useLiveQuery; confirm ilike source

useLiveQuery and collection are used elsewhere in the repo with these canonical imports; add them to the top of apps/dashboard/app/(app)/projects/_components/list/index.tsx. ilike was not found — confirm which package exports it before importing.

+import { collection } from "@/lib/collections";
+import { useLiveQuery } from "@tanstack/react-db";
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

42-47: Blocker: Hooks used at module scope; component wrapper missing.

useIsMobile (and later state access) are invoked outside any component, which will crash at runtime and fail TS/ESLint rules. Reintroduce the component function and the missing selection state.

Apply:

+export const DeploymentsList = () => {
-  const isCompactView = useIsMobile({ breakpoint: COMPACT_BREAKPOINT });
+  const isCompactView = useIsMobile({ breakpoint: COMPACT_BREAKPOINT });
+
+  const [selectedDeployment, setSelectedDeployment] = useState<{
+    deployment: Deployment;
+    environment?: Environment;
+  } | null>(null);

And ensure the closing brace at the end of the file closes this component (no extra stray brace). If this file previously exported the component, keep the named export.


47-55: Add missing type imports for Deployment and Environment.

Add import type { Deployment, Environment } from '@/lib/collections'; to the top of apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx.

apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (1)

169-177: 0 ms duration renders as (truthy check bug)

Treat 0 as a valid value.

Apply:

-          const parsedDuration = safeParseJson(log.request_body)?.duration;
+          const durationMs = safeParseJson(log.request_body)?.duration;
           return (
             <div className="font-mono">
-              {parsedDuration ? msToSeconds(parsedDuration) : "<EMPTY>"}
+              {typeof durationMs === "number" ? msToSeconds(durationMs) : "<EMPTY>"}
             </div>
           );
♻️ Duplicate comments (4)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)

132-147: CPU value missing in “Size” column.

The CPU span is empty, causing a blank “CPU” readout.

Apply (confirm the exact field name):

-                        <span className="font-semibold text-grayA-12 tabular-nums">
-
-                        </span>
+                        <span className="font-semibold text-grayA-12 tabular-nums">
+                          {deployment.runtimeConfig?.cpu ?? "—"}
+                        </span>

If the correct property is vCPU, cpus, or similar, adjust accordingly. I can update once you confirm the runtimeConfig shape.


298-299: Fix VirtualTable keyExtractor to use row.deployment.id.

Rows appear shaped as { deployment, environment }. Current extractor likely returns undefined, causing unstable keys.

Apply:

-      keyExtractor={(deployment) => deployment.id}
+      keyExtractor={(row) => row.deployment.id}
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)

95-97: Avoid O(N) scan per row; precompute Set of realtime IDs

Replace realtimeLogs.some(...) with Set.has(...) for O(1) checks.

Apply:

-        !realtimeLogs.some(
-          (realtime) => realtime.request_id === log.request_id
-        ) && ["opacity-50", "hover:opacity-100"],
+        !realtimeIds.has(log.request_id) && ["opacity-50", "hover:opacity-100"],

Support (outside selected lines, inside component):

const realtimeIds = useMemo(
  () => new Set(realtimeLogs.map((r) => r.request_id)),
  [realtimeLogs],
);

239-245: Footer counts: Good switch to shared formatter

This addresses the earlier request to avoid inline Intl.NumberFormat() in the footer.

🧹 Nitpick comments (6)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (2)

47-49: Empty description element

<Empty.Description> has no content; it adds noise/spacing. Remove or provide helpful copy.

-          <Empty.Description className="text-left">
-
-          </Empty.Description>
+          {/* Consider adding guidance or omit entirely */}

2-8: Unused imports (current state)

Dots is unused; ProjectActions and ProjectCard are unused until the populated-state render is restored.

-import { BookBookmark, Dots } from "@unkey/icons";
+import { BookBookmark } from "@unkey/icons";
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (1)

216-225: Remove duplicated typography classes around TimestampInfo.

Wrapper and child both set font-mono text-xs text-gray-9. Keep it in one place.

Apply:

-                        <div
-                          className={cn(
-                            "font-mono text-xs mt-1",
-                            "text-gray-9"
-                          )}
-                        >
+                        <div className="mt-1">
                           <TimestampInfo
                             value={deployment.createdAt}
                             className="font-mono text-xs text-gray-9"
                           />
                         </div>
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (3)

160-165: Avoid double JSON parse in the cell

Parse once and reuse.

Apply:

-        render: (log) => {
-          return (
-            <div className="font-mono">
-              {safeParseJson(log.response_body)?.limit ?? "<EMPTY>"}
-            </div>
-          );
-        },
+        render: (log) => {
+          const response = safeParseJson(log.response_body);
+          return (
+            <div className="font-mono">{response?.limit ?? "<EMPTY>"}</div>
+          );
+        },

190-196: Reuse parsedReset; remove fallback that can pass a string to TimestampInfo

Avoid reparsing and ensure type safety.

Apply:

-              <TimestampInfo
-                value={safeParseJson(log.response_body)?.reset ?? "<EMPTY>"}
+              <TimestampInfo
+                value={parsedReset}
                 className={cn(

200-201: Normalize placeholder casing

Use "" to match other cells.

Apply:

-            "<Empty>"
+            "<EMPTY>"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 470bc45 and 3d6e9b4.

📒 Files selected for processing (3)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (9 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (3 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (11 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/projects/_components/list/index.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/(app)/projects/_components/list/index.tsx
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2025-09-12T17:57:12.151Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx:111-116
Timestamp: 2025-09-12T17:57:12.151Z
Learning: In the Deployment type used across the dashboard deployment components, the source field is required and will always be present, so optional chaining (source?.branch) is not needed when accessing source.branch or source.gitSha.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
📚 Learning: 2025-08-25T13:46:08.303Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx:1-1
Timestamp: 2025-08-25T13:46:08.303Z
Learning: The NamespaceListDateTime component in apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-datetime/index.tsx is intentionally designed to use the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than the namespace list hook, as clarified by ogzhanolguncu. This coupling is by design, not an architectural issue.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2024-12-03T14:17:08.016Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/logs-page.tsx:77-83
Timestamp: 2024-12-03T14:17:08.016Z
Learning: The `<LogsTable />` component already implements virtualization to handle large datasets efficiently.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2025-08-25T13:46:34.441Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx:4-4
Timestamp: 2025-08-25T13:46:34.441Z
Learning: The namespace list refresh component (apps/dashboard/app/(app)/ratelimits/_components/controls/components/namespace-list-refresh.tsx) intentionally uses the overview hook (useFilters from @/app/(app)/ratelimits/[namespaceId]/_overview/hooks/use-filters) rather than a namespace-specific hook. This cross-coupling between namespace list components and overview hooks is an architectural design decision.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
📚 Learning: 2025-02-05T12:56:44.873Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2866
File: apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/log-details/components/log-footer.tsx:85-98
Timestamp: 2025-02-05T12:56:44.873Z
Learning: The RequestResponseDetails component in the ratelimit logs UI already handles empty content cases by preventing rendering when content is empty, so no additional empty state handling is needed in the parent components.

Applied to files:

  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
🧬 Code graph analysis (2)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/skeletons.tsx (1)
  • ActionColumnSkeleton (78-89)
apps/dashboard/lib/shorten-id.ts (1)
  • shortenId (5-54)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2)
apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts (1)
  • useRatelimitLogsQuery (17-208)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
🔇 Additional comments (10)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (4)

29-38: Dynamic import + skeleton fallback looks good.

Lazy-loading actions popover with SSR disabled is appropriate here.


77-82: Styling tweak LGTM.

Accent text for the Deployment ID improves contrast/readability.


159-167: Selection highlight depends on missing state.

Once the component/state fix is applied, this conditional class will work as intended.

After adding selectedDeployment state, verify row selection visual updates correctly on click.


305-308: Empty state copy LGTM.

Clear guidance and link to docs.

apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (6)

67-81: Hook wiring and query params: LGTM


119-121: Pointer-events gating on other rows: LGTM


131-135: Identifier cell reflow: LGTM


148-149: Status badge styling: LGTM


217-217: Columns memo deps: LGTM

Scoped to selection only; avoids unnecessary re-creation.


255-257: Empty state copy reflow: LGTM

import { safeParseJson } from "@/app/(app)/logs/utils";
import { VirtualTable } from "@/components/virtual-table/index";
import type { Column } from "@/components/virtual-table/types";
import { formatNumberFull } from "@/lib/fmt";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don't force en-US; honor user locale and cache formatter

Import looks good, but current formatNumberFull uses Intl.NumberFormat("en-US"), which conflicts with the PR goal of “locale-appropriate” formatting and allocates a formatter per call. Switch the helper to default-locale (or param) and cache per locale.

Support (outside selected lines, apps/dashboard/lib/fmt.ts):

-export function formatNumberFull(n: number): string {
-  return Intl.NumberFormat("en-US").format(n);
-}
+const nfCache = new Map<string | undefined, Intl.NumberFormat>();
+function getNF(locale?: string) {
+  if (!nfCache.has(locale)) nfCache.set(locale, new Intl.NumberFormat(locale));
+  return nfCache.get(locale)!;
+}
+export function formatNumberFull(n: number, locale?: string): string {
+  return getNF(locale).format(n);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { formatNumberFull } from "@/lib/fmt";
// apps/dashboard/lib/fmt.ts
const nfCache = new Map<string | undefined, Intl.NumberFormat>();
function getNF(locale?: string) {
if (!nfCache.has(locale)) nfCache.set(locale, new Intl.NumberFormat(locale));
return nfCache.get(locale)!;
}
export function formatNumberFull(n: number, locale?: string): string {
return getNF(locale).format(n);
}

neha added 2 commits September 18, 2025 07:48
- Use early return instead of break in getSelectedKeysState for better readability
- Apply code formatting fixes from biome
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ naaa760
❌ neha


neha seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (1)

101-110: Make the tooltip trigger keyboard-focusable; sync hover with focus (repeat of prior ask)

Add focusability + role/label to the trigger and set hover state on focus so keyboard users can reveal/toggle the checkbox.

           const iconContainer = (
             <div
               className={cn(
                 "size-5 rounded flex items-center justify-center cursor-pointer",
                 identity ? "bg-successA-3" : "bg-grayA-3",
                 isSelected && "bg-brand-5"
               )}
               onMouseEnter={() => setHoveredKeyId(key.id)}
               onMouseLeave={() => setHoveredKeyId(null)}
+              onFocus={() => setHoveredKeyId(key.id)}
+              onBlur={() => setHoveredKeyId(null)}
+              tabIndex={0}
+              role="button"
+              aria-label={identity ? "Key identity info" : "Key info"}
             >

Optionally reinforce at the clone site (works even if the container changes later):

-                  >
-                    {React.cloneElement(iconContainer, {
-                      className: cn(
-                        iconContainer.props.className,
-                        "cursor-pointer"
-                      ),
-                    })}
+                  >
+                    {React.cloneElement(iconContainer, {
+                      className: cn(iconContainer.props.className, "cursor-pointer"),
+                      tabIndex: 0,
+                      role: "button",
+                      "aria-label": "Key identity info",
+                    })}
                   </InfoTooltip>

Also applies to: 181-185

🧹 Nitpick comments (3)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)

4-4: Locale-aware number formatting is hard-coded to en-US; also re-instantiates per call

PR goal says “locale‑appropriate,” but formatNumberFull currently fixes locale to "en-US" and constructs a formatter per call. Prefer a single, memoized formatter defaulting to the user’s locale.

Proposed change in apps/dashboard/lib/fmt.ts:

-export function formatNumberFull(n: number): string {
-  return Intl.NumberFormat("en-US").format(n);
-}
+// Memoize once; default to user locale in browser, system default otherwise.
+const nfFull =
+  typeof window !== "undefined"
+    ? new Intl.NumberFormat(navigator.language)
+    : new Intl.NumberFormat(undefined);
+
+export function formatNumberFull(n: number): string {
+  return nfFull.format(n);
+}

Please confirm product intent: should counts follow the viewer’s locale or be normalized (e.g., always en-US)? If the latter, keep "en-US" but still memoize.


292-323: Fix edge case: returns “all-enabled” when none of the selected IDs exist in the current keys list

If selectedKeys contains only stale/filtered-out IDs, foundCount stays 0 and both flags remain true, yielding “all-enabled.” Return null in that case.

   const getSelectedKeysState = useCallback(() => {
     if (selectedKeys.size === 0) {
       return null;
     }

     let allEnabled = true;
     let allDisabled = true;
+    let foundCount = 0;

     for (const keyId of selectedKeys) {
       const key = keys.find((k) => k.id === keyId);
       if (!key) {
         continue;
       }
+      foundCount++;

       if (key.enabled) {
         allDisabled = false;
       } else {
         allEnabled = false;
       }

       // Early exit if we already found a mix
       if (!allEnabled && !allDisabled) {
         return "mixed";
       }
     }

+    if (foundCount === 0) {
+      return null;
+    }
     if (allEnabled) {
       return "all-enabled";
     }
-
-    return "all-disabled";
+    return "all-disabled";
   }, [selectedKeys, keys]);

351-357: Nit: consistent emphasis for both numbers

Style-only: wrap totalCount with the same accent as the first number for visual consistency.

               <span>of</span>
-              {formatNumberFull(totalCount)}
+              <span className="text-accent-12">
+                {formatNumberFull(totalCount)}
+              </span>
               <span>keys</span>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d6e9b4 and a01a4c2.

📒 Files selected for processing (13)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx (2 hunks)
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (13 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-client.tsx (2 hunks)
  • apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx (2 hunks)
  • apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx (2 hunks)
  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx (2 hunks)
  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (2 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx (2 hunks)
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx (2 hunks)
  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx (3 hunks)
  • apps/dashboard/components/virtual-table/components/loading-indicator.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
  • apps/dashboard/app/(app)/settings/root-keys/components/table/root-keys-list.tsx
  • apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
  • apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx
  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/apis/_components/api-list-client.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
  • apps/dashboard/components/virtual-table/components/loading-indicator.tsx
  • apps/dashboard/app/(app)/authorization/permissions/components/table/permissions-list.tsx
  • apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • apps/dashboard/app/(app)/logs/components/table/logs-table.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-24T14:34:30.621Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3115
File: apps/dashboard/components/logs/checkbox/filters-popover.tsx:33-55
Timestamp: 2025-04-24T14:34:30.621Z
Learning: In the ShortcutActivator component within filters-popover.tsx, the purpose is to track keys separately for each filter item, providing a registration mechanism for shortcuts passed to it rather than enforcing specific key combinations like option+shift+key.

Applied to files:

  • apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (3)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/hooks/use-keys-list-query.ts (1)
  • useKeysListQuery (13-85)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/hidden-value.tsx (1)
  • HiddenValueCell (5-47)
apps/dashboard/lib/fmt.ts (1)
  • formatNumberFull (5-7)
🔇 Additional comments (2)
apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/keys-list.tsx (2)

32-32: LGTM: dynamic import and loading placeholder

Named export selection and focus-visible styles on the fallback button look good.

Also applies to: 41-47, 50-50


112-116: LGTM: UI/UX polish

Loading state wrapper, identity badge styling, HiddenValueCell props split, adjusted paddings, and skeleton fallback condition read well.

Also applies to: 167-174, 224-228, 398-398, 408-418

neha added 2 commits September 18, 2025 08:04
- Use memoized number formatters instead of creating new instances
- Extract inline array to constant to avoid recreation
- Fix type safety by replacing 'any' with proper type assertion
- Apply code formatting fixes
@ogzhanolguncu
Copy link
Contributor

ogzhanolguncu commented Sep 18, 2025

If you sign the CLA above and resolve the coderabbits comments you fixed, we can move forward. And, please let's not fix anything other than formatting pagination numbers otherwise this becomes a nightmare to review and we might accidentally regress stuff.

@naaa760
Copy link
Author

naaa760 commented Sep 19, 2025

Hello @ogzhanolguncu
I’ve already signed the CLA and fixed the CodeRabbit comments. Let me know the next steps, or if any issue still comes up.

@@ -1,5 +1,4 @@
import { collection } from "@/lib/collections";
import { ilike, useLiveQuery } from "@tanstack/react-db";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical syntax errors: Missing required imports for collection, ilike, and useLiveQuery, and malformed JSX with missing closing </div> tag.

View Details
📝 Patch Details
diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx
index b0d569ece..e2d6fe4ea 100644
--- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx
+++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx
@@ -1,6 +1,8 @@
 
 import { BookBookmark, Dots } from "@unkey/icons";
 import { Button, Empty } from "@unkey/ui";
+import { ilike, useLiveQuery } from "@tanstack/react-db";
+import { collection } from "@/lib/collections";
 import { useProjectsFilters } from "../hooks/use-projects-filters";
 import { ProjectActions } from "./project-actions";
 import { ProjectCard } from "./projects-card";
@@ -101,6 +103,6 @@ export const ProjectsList = () => {
           />
         ))}
       </div>
-
+    </div>
   );
 };

Analysis

Critical syntax errors in ProjectsList component: Missing imports and unclosed JSX tag

What fails: File apps/dashboard/app/(app)/projects/_components/list/index.tsx has compilation-blocking errors preventing TypeScript compilation

How to reproduce:

cd apps/dashboard && pnpm run typecheck

Result: TypeScript compiler fails with errors:

  • Line 15: Cannot find name 'useLiveQuery'
  • Line 18: Cannot find name 'collection'
  • Line 19: Cannot find name 'ilike'
  • Line 70: JSX element 'div' has no corresponding closing tag

Expected: File should compile without TypeScript errors

Fix applied:

  • Added missing imports: import { ilike, useLiveQuery } from "@tanstack/react-db"; and import { collection } from "@/lib/collections";
  • Added missing closing </div> tag to complete JSX structure

import type { Column } from "@/components/virtual-table/types";
import { useIsMobile } from "@/hooks/use-mobile";
import type { Deployment, Environment } from "@/lib/collections";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing type imports cause compilation error: Deployment and Environment types are used but their import statement was removed.

View Details
📝 Patch Details
diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
index afe222b25..9876d5843 100644
--- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
+++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx
@@ -2,6 +2,7 @@
 import { VirtualTable } from "@/components/virtual-table/index";
 import type { Column } from "@/components/virtual-table/types";
 import { useIsMobile } from "@/hooks/use-mobile";
+import type { Deployment, Environment } from "@/lib/collections";
 
 import { shortenId } from "@/lib/shorten-id";
 import { BookBookmark, Cloud, CodeBranch, Cube } from "@unkey/icons";

Analysis

Missing type imports cause TypeScript compilation errors in deployments-list.tsx

What fails: TypeScript compilation fails with "Cannot find name 'Deployment'" and "Cannot find name 'Environment'" errors in deployments-list.tsx on lines 44-45 and throughout the component

How to reproduce:

cd apps/dashboard
pnpm exec tsc --noEmit --isolatedModules --jsx react-jsx app/\(app\)/projects/\[projectId\]/deployments/components/table/deployments-list.tsx

Result: TypeScript errors:

error TS2304: Cannot find name 'Deployment'.
error TS2304: Cannot find name 'Environment'.

Expected: Code should compile without type errors since the types are defined and exported from @/lib/collections

Fix: Added import type { Deployment, Environment } from "@/lib/collections"; to resolve the missing type definitions used in component state and prop declarations.

@@ -1,5 +1,4 @@
import { collection } from "@/lib/collections";
import { ilike, useLiveQuery } from "@tanstack/react-db";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing imports cause compilation error: collection, useLiveQuery, and ilike are used but not imported after the import statements were accidentally removed.

View Details
📝 Patch Details
diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx
index b0d569ece..e2d6fe4ea 100644
--- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx
+++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx
@@ -1,6 +1,8 @@
 
 import { BookBookmark, Dots } from "@unkey/icons";
 import { Button, Empty } from "@unkey/ui";
+import { ilike, useLiveQuery } from "@tanstack/react-db";
+import { collection } from "@/lib/collections";
 import { useProjectsFilters } from "../hooks/use-projects-filters";
 import { ProjectActions } from "./project-actions";
 import { ProjectCard } from "./projects-card";
@@ -101,6 +103,6 @@ export const ProjectsList = () => {
           />
         ))}
       </div>
-
+    </div>
   );
 };

Analysis

Missing imports cause TypeScript compilation errors in ProjectsList component

What fails: ProjectsList component in apps/dashboard/app/(app)/projects/_components/list/index.tsx uses useLiveQuery, collection.projects, and ilike on lines 15-19 without importing them, causing TypeScript compilation errors.

How to reproduce:

cd apps/dashboard && pnpm run typecheck

Result: TypeScript compiler reports:

app/(app)/projects/_components/list/index.tsx(15,20): error TS2304: Cannot find name 'useLiveQuery'.
app/(app)/projects/_components/list/index.tsx(18,26): error TS2304: Cannot find name 'collection'.
app/(app)/projects/_components/list/index.tsx(19,33): error TS2304: Cannot find name 'ilike'.

Expected: Clean compilation with proper imports from @tanstack/react-db and @/lib/collections per TanStack DB React documentation

@naaa760 naaa760 closed this by deleting the head repository Oct 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI: Log footer should format numbers

4 participants