Skip to content

feat(admin): show org slug in detail URLs instead of UUID#1763

Open
Shreyag02 wants to merge 8 commits into
mainfrom
feat/admin-org-slug-urls
Open

feat(admin): show org slug in detail URLs instead of UUID#1763
Shreyag02 wants to merge 8 commits into
mainfrom
feat/admin-org-slug-urls

Conversation

@Shreyag02

@Shreyag02 Shreyag02 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Admin organization detail pages now show the org's readable slug (its name) in the URL instead of the raw UUID
(e.g. /organizations/acme-corp instead of /organizations/6f3a…).
All API calls still run on the org id — the slug is display-only — so the change is purely about the URL, not the data layer.

Changes

  • Org detail URLs use the slug for a readable, shareable address.
  • The org id travels with in-app navigation (list, admins, create, audit) via router state, and is used for every org RPC.
  • Cold loads (refresh, bookmark, old UUID links) resolve the URL id/slug in one call, with a loader while it resolves.
  • Old /organizations/<uuid> links keep working and are rewritten in place to the canonical slug URL once resolved (disabled orgs stay on the id URL).
  • Breadcrumb navigates client-side (no full reload); tab links keep the slug from the current URL.

Technical Details

  • Why id, not slug, for RPCs:
    The id is the org's stable primary key and rides along with in-app navigation via router state, so every org RPC (getOrganization, KYC, members, roles, billing) uses the id directly and never pays a slug→id lookup. The slug is display-only.
  • How the id reaches the page:
    Navigators pass { state: { orgId } } alongside the slug URL. The detail page holds that id across tab switches (which clear router state).
  • Cold-load fallback:
    With no router state, the page resolves the URL segment via a single getOrganization call. The server's GetRaw accepts either form — a UUID (GetByID) or a slug (GetByName) — and returns disabled orgs too, so ids, enabled slugs, and disabled slugs all resolve in one request; only a genuinely unknown segment 404s and redirects to the list. Resolution runs in parallel with the id-scoped RPCs, so there's no per-load waterfall.
  • Canonical URL:
    An old UUID URL is replace-navigated to its slug once resolved (preserving the active sub-tab), so each org has a single live URL and the back-button stays sane. Disabled orgs are left on the id URL — the id is always a valid address for them.
  • Admins/audit views resolve the org by id (useOrganizationLookup) to display its title and build the slug link.

Note: an org's name (slug) is globally unique and is never freed — disabling only flips the org's state — so a slug never gets reassigned to a different org and bookmarks stay pointed at the same org.

Test Plan

  • Manual testing completed
    • List → org detail shows a slug URL; members/projects/tokens/etc. tabs load and stay on the slug.
    • Breadcrumb (org title + "Organizations") navigates without a full reload.
    • Audit log → organization link and Create org → redirect both land on the correct detail page.
    • Disabled org opened from the list works (id via state).
    • Refresh / paste a slug URL resolves (loader → page); old UUID link still works and rewrites in place to the slug; an unknown segment redirects to the list.
  • Build and type checking passes (admin app tsc clean; SDK rebuilt)

SQL Safety (if your PR touches *_repository.go or goqu.*)

N/A

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 21, 2026 9:16am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d27363fb-924c-46ef-bffa-e12b6f3f1043

📥 Commits

Reviewing files that changed from the base of the PR and between 1d779a6 and 78f2483.

📒 Files selected for processing (1)
  • web/apps/admin/src/pages/organizations/list/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/apps/admin/src/pages/organizations/list/index.tsx

📝 Walkthrough

Summary by CodeRabbit

  • Improvements
    • Organization navigation now uses readable names in URLs while retaining reliable organization identifiers.
    • Organization details deep links are more robust: org resolution prefers router-provided context, falls back to URL/query, and updates to canonical routes while staying consistent across tabs.
    • Admin “Organization” cells and organization actions now preserve org context during navigation.
    • Audit-log organization links (including side panels) now keep the selected organization context when moving to organization details.
    • Creating or selecting an organization opens details using the updated navigation behavior.

Walkthrough

Organization navigation now separates URL slugs from organization IDs carried in router state. Organization details resolve IDs from navigation state or URL lookup, while audit-log links propagate organization state through client-side navigation.

Changes

Organization-aware navigation

Layer / File(s) Summary
Organization navigation contracts and entry points
web/sdk/admin/hooks/useOrganizationLookup.ts, web/sdk/admin/views/admins/*, web/sdk/admin/views/organizations/list/*, web/apps/admin/src/pages/admins/AdminsPage.tsx, web/apps/admin/src/pages/organizations/list/index.tsx
Admin and organization list callbacks now pass (slug, orgId), use slugs in paths, preserve IDs in router state, and resolve organization names for admin cells.
Organization detail ID resolution
web/apps/admin/src/pages/organizations/details/index.tsx, web/sdk/admin/views/organizations/details/*
Organization details resolve IDs from router state or URL lookup, display loading or redirect states, and preserve slug-based tab and breadcrumb navigation.
Audit-log organization navigation
web/sdk/admin/views/audit-logs/*, web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
Audit-log navigation callbacks and side-panel links now accept optional organization state and forward it through client-side navigation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • raystack/frontier#1403: Both changes update organization detail navbar navigation and onNavigate propagation.
  • raystack/frontier#1410: Both changes update admin organization-link navigation contracts and target construction.

Suggested reviewers: rsbh

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
web/sdk/admin/views/organizations/details/layout/navbar.tsx (2)

241-243: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback to organizationId if the parsed segment is empty.

While currentPath usually includes the organization slug, adding an explicit fallback protects against edge cases where the extracted segment evaluates to an empty string (e.g., if the path exactly matches orgPrefix without a trailing segment).

♻️ Proposed refactor
   const orgSegment = currentPath.startsWith(orgPrefix)
-    ? currentPath.slice(orgPrefix.length).split("/")[0]
+    ? currentPath.slice(orgPrefix.length).split("/")[0] || organizationId
     : organizationId;

319-337: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract navigation paths to reduce duplication.

You can extract the string literals for the paths into variables to avoid duplicating them between the href and onNavigate callbacks.

♻️ Proposed refactor
@@ -311,27 +311,28 @@
     search.onChange(value);
   }
 
+  const listPath = `/${adminPaths.organizations}`;
+  const detailsPath = `${listPath}/${organization?.name || organization?.id}`;
+
   return (
     <nav className={styles.navbar}>
       <Flex gap={4} align="center">
         <Breadcrumb size="small">
           <Breadcrumb.Item
-            href={`/${adminPaths.organizations}`}
+            href={listPath}
             onClick={(e) => {
               e.preventDefault();
-              onNavigate(`/${adminPaths.organizations}`);
+              onNavigate(listPath);
             }}
             leadingIcon={<OrganizationIcon />}
           >
             {t.organization({ plural: true, case: "capital" })}
           </Breadcrumb.Item>
           <Breadcrumb.Separator />
           {/* Navigate client-side so we don't full-reload and lose the held org id. */}
           <Breadcrumb.Item
-            href={`/${adminPaths.organizations}/${organization?.name || organization?.id}`}
+            href={detailsPath}
             onClick={(e) => {
               e.preventDefault();
-              onNavigate(
-                `/${adminPaths.organizations}/${organization?.name || organization?.id}`,
-              );
+              onNavigate(detailsPath);
             }}
             leadingIcon={

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 95db9072-ec2e-4afc-8448-2a4967c46c75

📥 Commits

Reviewing files that changed from the base of the PR and between 7dcfcee and 37fdcad.

📒 Files selected for processing (13)
  • web/apps/admin/src/pages/admins/AdminsPage.tsx
  • web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/apps/admin/src/pages/organizations/list/index.tsx
  • web/sdk/admin/hooks/useOrganizationLookup.ts
  • web/sdk/admin/views/admins/columns.tsx
  • web/sdk/admin/views/admins/index.tsx
  • web/sdk/admin/views/audit-logs/index.tsx
  • web/sdk/admin/views/audit-logs/sidepanel-details.tsx
  • web/sdk/admin/views/audit-logs/sidepanel-list-link.tsx
  • web/sdk/admin/views/organizations/details/layout/navbar.tsx
  • web/sdk/admin/views/organizations/list/create.tsx
  • web/sdk/admin/views/organizations/list/index.tsx

Comment thread web/apps/admin/src/pages/organizations/details/index.tsx Outdated
@coveralls

coveralls commented Jul 16, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29817588770

Coverage remained the same at 46.201%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 38436
Covered Lines: 17758
Line Coverage: 46.2%
Coverage Strength: 13.36 hits per line

💛 - Coveralls

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0c0b34e-4e9d-45b3-8cc3-956eb05b542a

📥 Commits

Reviewing files that changed from the base of the PR and between 29bb31a and 1d779a6.

📒 Files selected for processing (1)
  • web/apps/admin/src/pages/organizations/details/index.tsx

Comment on lines +86 to +105
useEffect(() => {
if (paramIsId && org?.name && org.state !== 'disabled') {
const rest = location.pathname.slice(
location.pathname.indexOf(urlParam!) + urlParam!.length,
);
navigate(`/${paths.organizations}/${org.name}${rest}`, {
replace: true,
state: { orgId },
});
}
}, [
paramIsId,
org?.name,
org?.state,
orgId,
urlParam,
location.pathname,
navigate,
paths.organizations,
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Canonical-URL rewrite drops the query string and hash.

rest is sliced from location.pathname only, so a UUID bookmark like /organizations/{id}/members?page=2 rewrites to /organizations/{slug}/members, silently losing ?page=2. Safe fallback (page still loads), but any tab relying on query-string state (filters, pagination) loses it on this one-time redirect.

Proposed fix
   useEffect(() => {
     if (paramIsId && org?.name && org.state !== 'disabled') {
-      const rest = location.pathname.slice(
-        location.pathname.indexOf(urlParam!) + urlParam!.length,
-      );
-      navigate(`/${paths.organizations}/${org.name}${rest}`, {
+      const restPath = location.pathname.slice(
+        location.pathname.indexOf(urlParam!) + urlParam!.length,
+      );
+      navigate(
+        `/${paths.organizations}/${org.name}${restPath}${location.search}${location.hash}`,
+        {
           replace: true,
           state: { orgId },
-      });
+        },
+      );
     }
   }, [
     paramIsId,
     org?.name,
     org?.state,
     orgId,
     urlParam,
     location.pathname,
+    location.search,
+    location.hash,
     navigate,
     paths.organizations,
   ]);
📝 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
useEffect(() => {
if (paramIsId && org?.name && org.state !== 'disabled') {
const rest = location.pathname.slice(
location.pathname.indexOf(urlParam!) + urlParam!.length,
);
navigate(`/${paths.organizations}/${org.name}${rest}`, {
replace: true,
state: { orgId },
});
}
}, [
paramIsId,
org?.name,
org?.state,
orgId,
urlParam,
location.pathname,
navigate,
paths.organizations,
]);
useEffect(() => {
if (paramIsId && org?.name && org.state !== 'disabled') {
const restPath = location.pathname.slice(
location.pathname.indexOf(urlParam!) + urlParam!.length,
);
navigate(
`/${paths.organizations}/${org.name}${restPath}${location.search}${location.hash}`,
{
replace: true,
state: { orgId },
},
);
}
}, [
paramIsId,
org?.name,
org?.state,
orgId,
urlParam,
location.pathname,
location.search,
location.hash,
navigate,
paths.organizations,
]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants