feat(admin-ui): runtime terminology customization for admin UI#1454
feat(admin-ui): runtime terminology customization for admin UI#1454rohilsurana wants to merge 11 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a dev JSON config and a Vite dev plugin; introduces AdminConfigProvider, terminology and path hooks (useAdminTerminology, useAdminPaths) in the SDK; wires these into the admin app, replacing many hard-coded labels and route segments with dynamic terminology and path slugs. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add a context-based terminology system that allows deployments to customize entity labels (e.g. Organization → Workspace) and URL paths across the entire admin UI via the /configs endpoint. - Add AdminConfigContext, useAdminTerminology hook, and useAdminPaths hook - Replace all hardcoded entity labels in 44 admin view files - Make route paths dynamic based on terminology config - Add Vite dev server middleware to serve configs.dev.json locally
7fe0837 to
7ad2980
Compare
Pull Request Test Coverage Report for Build 23194563756Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx (1)
53-77:⚠️ Potential issue | 🟡 MinorInclude terminology in
addMemberdependencies to avoid stale toast text.
addMembercaptures terminology, but its dependency list does not include it. After runtime/configschanges, toast copy can remain stale until remount.Suggested fix
export function useAddProjectMembers({ projectId }: useAddProjectMembersProps) { const t = useAdminTerminology(); + const memberLabel = t.member({ case: "capital" }); const { orgMembersMap } = useContext(OrganizationContext); @@ - toast.success(`${t.member({ case: "capital" })} added`); + toast.success(`${memberLabel} added`); @@ - [projectId, createPolicy, refetch, projectMembers], + [projectId, createPolicy, refetch, projectMembers, memberLabel], );web/sdk/admin/views/organizations/details/layout/index.tsx (1)
35-70:⚠️ Potential issue | 🟡 MinorFound-state page title still bypasses runtime terminology.
Line 69 hardcodes
"Organizations", so this page title won’t reflect customized terminology (e.g., “Workspaces”).Suggested fix
- const title = `${organization?.title} | Organizations`; + const title = `${organization?.title} | ${t.organization({ plural: true, case: "capital" })}`;web/sdk/admin/views/users/list/invite-users.tsx (1)
206-224:⚠️ Potential issue | 🟠 MajorOrganization field placeholder is still hardcoded.
The label is terminology-aware, but the placeholder still says
"Select an Organization", so this dialog is only partially customizable.Suggested fix
- <Select.Value placeholder="Select an Organization" /> + <Select.Value + placeholder={`Select ${t.organization({ case: "capital" })}`} + />
🧹 Nitpick comments (4)
web/sdk/admin/views/organizations/details/layout/invite-users-dialog.tsx (1)
69-69: Use plural terminology when multiple invitations succeedLine 69 always uses singular user terminology, but this flow accepts multiple emails. Consider pluralizing based on response count for better UX consistency.
Proposed refactor
- onSuccess: () => { + onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: createConnectQueryKey({ schema: FrontierServiceQueries.listOrganizationInvitations, transport, input: { orgId: organizationId }, cardinality: "finite", }), }); - toast.success(`${t.user({ case: "capital" })} invited`); + const inviteCount = data?.invitations?.length ?? 0; + toast.success( + `${t.user({ case: "capital", plural: inviteCount > 1 })} invited`, + ); onOpenChange(false); },web/apps/admin/src/utils/constants.ts (1)
44-51: Avoid duplicating terminology defaults across app and sdk constants.This
defaultTerminologycan drift fromweb/sdk/admin/utils/constants.ts, leading to inconsistent behavior. Prefer a shared source for terminology defaults/types.Also applies to: 61-61
web/sdk/admin/views/organizations/details/apis/columns.tsx (1)
9-9: Use a type-only import forTerminologyEntity.
TerminologyEntityis used only as a type annotation (in theColumnOptionsinterface), soimport typeis more appropriate and makes the intent explicit.Suggested patch
-import { TerminologyEntity } from "../../../../hooks/useAdminTerminology"; +import type { TerminologyEntity } from "../../../../hooks/useAdminTerminology";web/sdk/admin/hooks/useAdminTerminology.ts (1)
22-23: The "capital" case lowercases subsequent characters, which may not be intended for multi-word terms.If a deployment customizes terminology with multi-word values like "Service Account", applying
case: "capital"will produce "Service account" instead of preserving "Service Account". Consider whether this is the desired behavior.♻️ Alternative: preserve original casing after first character
case "capital": - return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); + return text.charAt(0).toUpperCase() + text.slice(1);
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1a551206-c73a-4949-9209-8c6b23aae0f5
📒 Files selected for processing (48)
web/apps/admin/configs.dev.jsonweb/apps/admin/src/components/Sidebar/index.tsxweb/apps/admin/src/contexts/App.tsxweb/apps/admin/src/pages/admins/AdminsPage.tsxweb/apps/admin/src/pages/organizations/list/index.tsxweb/apps/admin/src/pages/users/UsersPage.tsxweb/apps/admin/src/routes.tsxweb/apps/admin/src/utils/constants.tsweb/apps/admin/vite.config.tsweb/sdk/admin/contexts/AdminConfigContext.tsxweb/sdk/admin/hooks/useAdminPaths.tsweb/sdk/admin/hooks/useAdminTerminology.tsweb/sdk/admin/index.tsweb/sdk/admin/utils/constants.tsweb/sdk/admin/views/admins/columns.tsxweb/sdk/admin/views/admins/index.tsxweb/sdk/admin/views/audit-logs/columns.tsxweb/sdk/admin/views/audit-logs/index.tsxweb/sdk/admin/views/audit-logs/sidepanel-details.tsxweb/sdk/admin/views/invoices/columns.tsxweb/sdk/admin/views/invoices/index.tsxweb/sdk/admin/views/organizations/details/apis/columns.tsxweb/sdk/admin/views/organizations/details/apis/details-dialog.tsxweb/sdk/admin/views/organizations/details/apis/index.tsxweb/sdk/admin/views/organizations/details/edit/organization.tsxweb/sdk/admin/views/organizations/details/invoices/index.tsxweb/sdk/admin/views/organizations/details/layout/index.tsxweb/sdk/admin/views/organizations/details/layout/invite-users-dialog.tsxweb/sdk/admin/views/organizations/details/layout/navbar.tsxweb/sdk/admin/views/organizations/details/members/remove-member.tsxweb/sdk/admin/views/organizations/details/projects/columns.tsxweb/sdk/admin/views/organizations/details/projects/index.tsxweb/sdk/admin/views/organizations/details/projects/members/remove-member.tsxweb/sdk/admin/views/organizations/details/projects/rename-project.tsxweb/sdk/admin/views/organizations/details/projects/use-add-project-members.tsxweb/sdk/admin/views/organizations/details/security/block-organization.tsxweb/sdk/admin/views/organizations/details/side-panel/index.tsxweb/sdk/admin/views/organizations/details/tokens/columns.tsxweb/sdk/admin/views/organizations/details/tokens/index.tsxweb/sdk/admin/views/organizations/list/create.tsxweb/sdk/admin/views/organizations/list/index.tsxweb/sdk/admin/views/organizations/list/navbar.tsxweb/sdk/admin/views/users/details/layout/navbar.tsxweb/sdk/admin/views/users/details/layout/suspend-user.tsxweb/sdk/admin/views/users/details/security/block-user.tsxweb/sdk/admin/views/users/details/user-details.tsxweb/sdk/admin/views/users/list/invite-users.tsxweb/sdk/admin/views/users/list/list.tsx
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/sdk/admin/hooks/useAdminTerminology.ts (1)
44-69:⚠️ Potential issue | 🟠 MajorMemoization is still tied to unstable object identity.
Line 69 uses
[terminology]as the dependency. Inweb/sdk/admin/contexts/AdminConfigContext.tsx(Line 12-20),mergedConfig.terminologyis reconstructed, so this hook can still return a freshtobject on provider re-renders even when terminology values are unchanged.🔧 Proposed fix (depend on primitive terminology values)
export const useAdminTerminology = () => { const config = useAdminConfig(); const terminology = config.terminology || defaultTerminology; + const organizationSingular = + terminology.organization?.singular ?? defaultTerminology.organization.singular; + const organizationPlural = + terminology.organization?.plural ?? defaultTerminology.organization.plural; + const projectSingular = + terminology.project?.singular ?? defaultTerminology.project.singular; + const projectPlural = + terminology.project?.plural ?? defaultTerminology.project.plural; + const teamSingular = + terminology.team?.singular ?? defaultTerminology.team.singular; + const teamPlural = + terminology.team?.plural ?? defaultTerminology.team.plural; + const memberSingular = + terminology.member?.singular ?? defaultTerminology.member.singular; + const memberPlural = + terminology.member?.plural ?? defaultTerminology.member.plural; + const userSingular = + terminology.user?.singular ?? defaultTerminology.user.singular; + const userPlural = + terminology.user?.plural ?? defaultTerminology.user.plural; + const appName = terminology.appName ?? defaultTerminology.appName; return useMemo(() => ({ - organization: createEntity( - terminology.organization?.singular || defaultTerminology.organization.singular, - terminology.organization?.plural || defaultTerminology.organization.plural - ), - project: createEntity( - terminology.project?.singular || defaultTerminology.project.singular, - terminology.project?.plural || defaultTerminology.project.plural - ), - team: createEntity( - terminology.team?.singular || defaultTerminology.team.singular, - terminology.team?.plural || defaultTerminology.team.plural - ), - member: createEntity( - terminology.member?.singular || defaultTerminology.member.singular, - terminology.member?.plural || defaultTerminology.member.plural - ), - user: createEntity( - terminology.user?.singular || defaultTerminology.user.singular, - terminology.user?.plural || defaultTerminology.user.plural - ), - appName: createEntity( - terminology.appName || defaultTerminology.appName, - terminology.appName || defaultTerminology.appName - ), - }),[terminology]); + organization: createEntity(organizationSingular, organizationPlural), + project: createEntity(projectSingular, projectPlural), + team: createEntity(teamSingular, teamPlural), + member: createEntity(memberSingular, memberPlural), + user: createEntity(userSingular, userPlural), + appName: createEntity(appName, appName), + }), [ + organizationSingular, + organizationPlural, + projectSingular, + projectPlural, + teamSingular, + teamPlural, + memberSingular, + memberPlural, + userSingular, + userPlural, + appName, + ]); };Verification script (read-only) to confirm current dependency behavior and provider reconstruction pattern:
#!/bin/bash set -euo pipefail echo "== Checking AdminConfigProvider terminology reconstruction ==" fd 'AdminConfigContext.tsx$' -t f | while read -r f; do echo "-- $f" rg -n -C2 'merge\(\{\}, defaultTerminology, config\.terminology\)|AdminConfigContext\.Provider value' "$f" done echo echo "== Checking useAdminTerminology memo dependencies ==" fd 'useAdminTerminology.ts$' -t f | while read -r f; do echo "-- $f" rg -n -C2 'return useMemo\(|\]\s*,\s*\[terminology\]|\),\s*\[terminology\]' "$f" doneExpected result: provider rebuilds terminology object and hook memo depends on that object identity.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 03f37194-74da-486d-bc9c-2f5fe67608f9
📒 Files selected for processing (1)
web/sdk/admin/hooks/useAdminTerminology.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web/sdk/admin/views/organizations/details/layout/index.tsx (1)
69-69: Consider moving title computation inside the conditional branch.The
titlevariable is computed unconditionally but only used whenorganizationis truthy (line 77). While not a bug (the value is never rendered when undefined), moving this inside the branch improves clarity and avoids evaluatingorganization?.titleunnecessarily.♻️ Optional: Move title computation
Move the title computation inside the
organization ?branch, or compute it inline at line 77:- const title = `${organization?.title} | ${t.organization({ case: "capital", plural: true })}`; - return isLoading ? ( <Flex align="center" justify="center" style={{ minHeight: "200px", width: "100%" }}> <Spinner size={6} /> </Flex> ) : organization ? ( <Flex direction="column" className={styles.page}> - <PageTitle title={title} /> + <PageTitle title={`${organization.title} | ${t.organization({ case: "capital", plural: true })}`} />
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4f0743a3-ad29-4f99-bf9e-e7a910bbbb3d
📒 Files selected for processing (3)
web/sdk/admin/views/organizations/details/layout/index.tsxweb/sdk/admin/views/organizations/details/projects/use-add-project-members.tsxweb/sdk/admin/views/users/list/invite-users.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx
…SDK and admin Move applyCase, createEntity, types, and createTerminologyMap into shared/terminology.ts. Both useTerminology (react SDK) and useAdminTerminology (admin SDK) now delegate to the same core logic. Existing imports are preserved via re-exports.
Replace useAdminTerminology with a single shared useTerminology hook. Add TerminologyProvider to shared/terminology.tsx — both CustomizationProvider (react SDK) and AdminConfigProvider (admin) wrap their children with it, so useTerminology works everywhere.
Summary
/configsendpointuseTerminologyhook across both the react SDK and admin SDK via a sharedTerminologyProviderWhat changed
Shared infrastructure (
web/sdk/shared/terminology.tsx):TerminologyProvidercontext +useTerminologyhook — single source of truth for both SDKscreateTerminologyMaputility,TerminologyEntitytypesAdmin SDK (
web/sdk/admin/):AdminConfigProviderwraps children withTerminologyProvideruseAdminPathshook — generates URL-safe slugs from terminology for dynamic routinguseTerminology()React SDK (
web/sdk/react/):CustomizationProviderwraps children withTerminologyProvideruseTerminologyre-exported from shared (existing imports unchanged)Admin app (
web/apps/admin/):useAdminPathsconfigs.dev.jsonat/configsfor local developmentDocs:
docs/docs/sdk/web/admin/utilities.mdupdated withuseTerminologyanduseAdminPathsusageUsage
Configure via
/configsendpoint response:{ "terminology": { "organization": { "singular": "Workspace", "plural": "Workspaces" }, "user": { "singular": "Person", "plural": "People" } } }In views:
Test plan
pnpm run buildinweb/sdk— passespnpm run buildinweb/apps/admin— passeshttp://localhost:5173/configs— returns JSON fromconfigs.dev.jsonconfigs.dev.jsonto use "Workspace" terminology, refresh — sidebar, page titles, breadcrumbs, table headers, URLs all show "Workspaces"useTerminologystill works unchanged