From 45a7b4925ba4efd4fe912b1d5c043e07c683c7ed Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 11:53:20 -0700 Subject: [PATCH] additions: sub-org ui updates --- backend/src/ee/routes/v1/sub-org-router.ts | 3 +- .../components/v2/PageHeader/PageHeader.tsx | 2 +- frontend/src/config/request.ts | 13 +- .../hooks/api/subOrganizations/mutations.tsx | 5 +- .../src/hooks/api/subOrganizations/types.ts | 1 + .../components/NavBar/Navbar.tsx | 377 +++++++++++------- .../NavBar/NewSubOrganizationForm.tsx | 15 +- .../components/OrgNavBar/OrgNavBar.tsx | 18 +- .../AccessManagementPage.tsx | 10 +- .../OrgRoleTabSection/OrgRoleTable.tsx | 6 +- .../AppConnectionsPage/AppConnectionsPage.tsx | 5 +- .../AuditLogsPage/AuditLogsPage.tsx | 5 +- .../GroupDetailsByIDPage.tsx | 10 +- .../IdentityDetailsByIDPage.tsx | 8 +- .../NetworkingPage/NetworkingPage.tsx | 5 +- .../NetworkingTabGroup/NetworkingTabGroup.tsx | 5 +- .../ProjectsPage/ProjectsPage.tsx | 6 +- .../RoleByIDPage/RoleByIDPage.tsx | 4 +- .../SecretSharingPage/SecretSharingPage.tsx | 5 +- .../SecretSharingPage/ShareSecretSection.tsx | 8 +- .../SecretSharingSettingsPage.tsx | 7 +- .../SettingsPage/SettingsPage.tsx | 4 +- .../components/OrgTabGroup/OrgTabGroup.tsx | 2 +- .../UserDetailsByIDPage.tsx | 6 +- .../src/pages/public/ErrorPage/ErrorPage.tsx | 2 +- 25 files changed, 342 insertions(+), 190 deletions(-) diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index c89fa40a3..200130488 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -13,7 +13,8 @@ const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({ name: true, slug: true, createdAt: true, - updatedAt: true + updatedAt: true, + parentOrgId: true }); export const registerSubOrgRouter = async (server: FastifyZodProvider) => { diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 3c9e743ff..e3f72f61b 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -24,7 +24,7 @@ const SCOPE_NAME: Record, { label: string; icon: Ico [ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.SecretScanning]: { label: "Project", icon: faCube }, - namespace: { label: "Namespace", icon: faCubes }, + namespace: { label: "Sub-Organization", icon: faCubes }, instance: { label: "Server", icon: faServer } }; diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index 3fc01ccac..16b8b4f45 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -40,9 +40,16 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } - const subOrganization = params.get("subOrganization"); - if (subOrganization) { - config.headers.set("x-infisical-org", subOrganization); + + const rootOrgHeader = config.headers.get("x-root-org"); + + if (rootOrgHeader) { + config.headers.delete("x-root-org"); + } else { + const subOrganization = params.get("subOrganization"); + if (subOrganization) { + config.headers.set("x-infisical-org", subOrganization); + } } } diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx index f2b9ac7a8..81369a62e 100644 --- a/frontend/src/hooks/api/subOrganizations/mutations.tsx +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -11,7 +11,10 @@ export const useCreateSubOrganization = () => { mutationFn: async (dto: TCreateSubOrganizationDTO) => { const { data } = await apiRequest.post<{ organization: TSubOrganization }>( "/api/v1/sub-organizations", - dto + dto, + { + headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header + } ); return data; }, diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts index 6086830fb..e9b3f2f01 100644 --- a/frontend/src/hooks/api/subOrganizations/types.ts +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -4,6 +4,7 @@ export type TSubOrganization = { slug: string; createdAt: string; updatedAt: string; + parentOrgId: string; }; export type TCreateSubOrganizationDTO = { diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 0bc4439ae..ec0ba3737 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -6,6 +6,7 @@ import { faBook, faCaretDown, faCheck, + faChevronRight, faCubes, faEnvelope, faExclamationTriangle, @@ -59,7 +60,7 @@ import { import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; -import { SubscriptionPlan } from "@app/hooks/api/types"; +import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; @@ -131,7 +132,9 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); + + console.log("current", currentOrg, isSubOrganization); const [showAdminsModal, setShowAdminsModal] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false); @@ -155,6 +158,7 @@ export const Navbar = () => { const [shouldShowMfa, toggleShowMfa] = useToggle(false); const router = useRouter(); const queryClient = useQueryClient(); + const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false); const location = useLocation(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -224,6 +228,33 @@ export const Navbar = () => { const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate? + const handleOrgNav = async (org: Organization) => { + if (currentOrg?.id === org.id) return; + + if (org.authEnforced) { + // org has an org-level auth method enabled (e.g. SAML) + // -> logout + redirect to SAML SSO + + await logout.mutateAsync(); + if (org.orgAuthMethod === AuthMethod.OIDC) { + window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`); + } else { + window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`); + } + window.close(); + return; + } + + if (org.googleSsoAuthEnforced) { + await logout.mutateAsync(); + window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); + window.close(); + return; + } + + handleOrgChange(org?.id); + }; + return (
@@ -253,38 +284,46 @@ export const Navbar = () => { ) : ( <>
- - -
- - -

{currentOrg?.name}

-
-
- {getPlan(subscription)} -
- {subscription.cardDeclined && ( - -
- -
-
+ +
+ { + navigate({ + to: "/organization/projects", + search: (search) => ({ ...search, subOrganization: undefined }) + }); + if (isSubOrganization) { + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + } + }} + variant="org" + className={twMerge( + "max-w-full min-w-0 cursor-pointer text-sm", + (!isOrgScope || isSubOrganization) && + "bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" )} + > + +

{currentOrg?.name}

+
+
+ {getPlan(subscription)}
- + {subscription.cardDeclined && ( + +
+ +
+
+ )} +
{
- {subscription?.subOrganization && ( - <> - - - - - - } - onClick={() => setShowSubOrgForm(true)} - > - New Sub Organization - - {Boolean(subOrganizations.length) && ( -
- )} - {subOrganizations?.map((org) => { - return ( - - - - ); - })} - - -
- - )}
- organizations + Organizations
{orgs?.map((org) => { + if ( + subscription.subOrganization && + (org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId) + ) { + return ( + + { + setIsOrgSelectOpen(false); + handleOrgNav(org); + }} + className="cursor-pointer font-normal" + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+ +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + ); + } + return ( - - + handleOrgNav(org)} + className="cursor-pointer font-normal" + key={org.id} + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+
); })} @@ -432,6 +440,79 @@ export const Navbar = () => {
+ {currentOrg.subOrganization && ( + <> +

/

+ + + + +

{currentOrg.subOrganization.name}

+
+ + +
+ + + +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + + )} {!isOrgScope && ( <>

/

@@ -654,7 +735,11 @@ export const Navbar = () => { subTitle="Define a new sub-organization under your current organization." >
- setShowSubOrgForm(false)} /> + { + setShowSubOrgForm(false); + }} + />
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index 5a736491d..32c557735 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -1,5 +1,7 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -31,9 +33,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { resolver: zodResolver(AddOrgSchema) }); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const router = useRouter(); + const onSubmit = async ({ name }: FormData) => { try { - await createSubOrg.mutateAsync({ + const { organization } = await createSubOrg.mutateAsync({ name }); @@ -42,6 +48,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { text: "Successfully created sub organization" }); onClose(); + + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: organization.name }) + }); + queryClient.clear(); + await router.invalidate({ sync: true }).catch(() => null); } catch { createNotification({ text: "Failed to create sub organization", diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index 4e72f9b06..564970904 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -16,6 +16,8 @@ export const OrgNavBar = ({ isHidden }: Props) => { const { pathname } = useLocation(); + const variant = isRootOrganization ? "org" : "namespace"; + return ( <> {!isHidden && ( @@ -32,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( - + Overview )} {({ isActive }) => ( - + App Connections )} {({ isActive }) => ( - + Networking )} {({ isActive }) => ( - + Secret Sharing )} @@ -61,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( { {({ isActive }) => ( - + Audit Logs )} @@ -85,7 +87,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {isRootOrganization && ( {({ isActive }) => ( - + Usage & Billing )} @@ -93,7 +95,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { )} {({ isActive }) => ( - + Settings )} diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 8059f71ce..d3e93bcea 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from " export const AccessManagementPage = () => { const { t } = useTranslation(); const { permission } = useOrgPermission(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const navigate = useNavigate({ from: ROUTE_PATHS.Organization.AccessControlPage.path @@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => { {tabSections .filter((el) => !el.isHidden) .map((el) => ( - + {el.label} ))} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 03de66a38..6aaae7d57 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -68,7 +68,7 @@ enum RolesOrderBy { export const OrgRoleTable = () => { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -200,7 +200,9 @@ export const OrgRoleTable = () => { return (
-

Organization Roles

+

+ {isSubOrganization ? "Sub-" : ""}Organization Roles +

{(isAllowed) => (