additions: sub-org ui updates

This commit is contained in:
Scott Wilson
2025-10-20 11:53:20 -07:00
parent 9db8f2a87d
commit 45a7b4925b
25 changed files with 342 additions and 190 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({
name: true, name: true,
slug: true, slug: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
parentOrgId: true
}); });
export const registerSubOrgRouter = async (server: FastifyZodProvider) => { export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
@@ -24,7 +24,7 @@ const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: Ico
[ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { 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 } instance: { label: "Server", icon: faServer }
}; };
+10 -3
View File
@@ -40,9 +40,16 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`; config.headers.Authorization = `Bearer ${providerAuthToken}`;
} }
const subOrganization = params.get("subOrganization");
if (subOrganization) { const rootOrgHeader = config.headers.get("x-root-org");
config.headers.set("x-infisical-org", subOrganization);
if (rootOrgHeader) {
config.headers.delete("x-root-org");
} else {
const subOrganization = params.get("subOrganization");
if (subOrganization) {
config.headers.set("x-infisical-org", subOrganization);
}
} }
} }
@@ -11,7 +11,10 @@ export const useCreateSubOrganization = () => {
mutationFn: async (dto: TCreateSubOrganizationDTO) => { mutationFn: async (dto: TCreateSubOrganizationDTO) => {
const { data } = await apiRequest.post<{ organization: TSubOrganization }>( const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
"/api/v1/sub-organizations", "/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; return data;
}, },
@@ -4,6 +4,7 @@ export type TSubOrganization = {
slug: string; slug: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
parentOrgId: string;
}; };
export type TCreateSubOrganizationDTO = { export type TCreateSubOrganizationDTO = {
@@ -6,6 +6,7 @@ import {
faBook, faBook,
faCaretDown, faCaretDown,
faCheck, faCheck,
faChevronRight,
faCubes, faCubes,
faEnvelope, faEnvelope,
faExclamationTriangle, faExclamationTriangle,
@@ -59,7 +60,7 @@ import {
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery"; 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 { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
@@ -131,7 +132,9 @@ export const INFISICAL_SUPPORT_OPTIONS = [
export const Navbar = () => { export const Navbar = () => {
const { user } = useUser(); const { user } = useUser();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
console.log("current", currentOrg, isSubOrganization);
const [showAdminsModal, setShowAdminsModal] = useState(false); const [showAdminsModal, setShowAdminsModal] = useState(false);
const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showSubOrgForm, setShowSubOrgForm] = useState(false);
@@ -155,6 +158,7 @@ export const Navbar = () => {
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false);
const location = useLocation(); const location = useLocation();
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); 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 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 ( return (
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1"> <div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
<div className="mr-auto flex items-center overflow-hidden"> <div className="mr-auto flex items-center overflow-hidden">
@@ -253,38 +284,46 @@ export const Navbar = () => {
) : ( ) : (
<> <>
<div className="flex items-center overflow-hidden"> <div className="flex items-center overflow-hidden">
<DropdownMenu modal={false}> <DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<Link className="overflow-hidden" to="/organization/projects"> <div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary"> <Badge
<Badge onClick={async () => {
variant="org" navigate({
className={twMerge( to: "/organization/projects",
"max-w-full min-w-0 cursor-pointer text-sm", search: (search) => ({ ...search, subOrganization: undefined })
!isOrgScope && });
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" if (isSubOrganization) {
)} queryClient.clear();
> await router.invalidate({ sync: true }).catch(() => null);
<FontAwesomeIcon icon={faGlobe} /> }
<p className="truncate">{currentOrg?.name}</p> }}
</Badge> variant="org"
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!"> className={twMerge(
{getPlan(subscription)} "max-w-full min-w-0 cursor-pointer text-sm",
</div> (!isOrgScope || isSubOrganization) &&
{subscription.cardDeclined && ( "bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)} )}
>
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div> </div>
</Link> {subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)}
</div>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div> <div>
<IconButton <IconButton
@@ -298,127 +337,96 @@ export const Navbar = () => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="start" align="center"
side="bottom" side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md" className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }} style={{ minWidth: "220px" }}
> >
{subscription?.subOrganization && (
<>
<DropdownSubMenu>
<DropdownSubMenuTrigger>
<Button
onClick={async () => {}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={<FontAwesomeIcon icon={faCubes} className="mr-3" />}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
Sub Organizations
</div>
</Button>
</DropdownSubMenuTrigger>
<DropdownSubMenuContent
alignOffset={-30}
sideOffset={5}
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub Organization
</DropdownMenuItem>
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
{subOrganizations?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.parentOrgId === org.id && (
<FontAwesomeIcon
icon={faCheck}
className="mr-3 text-primary"
/>
)
}
onClick={async () => {
navigate({
to: "/organization/projects",
search: (prev) => ({ ...prev, subOrganization: org.name })
});
queryClient.clear();
await router.invalidate({ sync: true }).catch(() => null);
}}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
</DropdownSubMenuContent>
</DropdownSubMenu>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
</>
)}
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize"> <div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
organizations Organizations
</div> </div>
{orgs?.map((org) => { {orgs?.map((org) => {
if (
subscription.subOrganization &&
(org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId)
) {
return (
<DropdownSubMenu key={`${org.id}-sub-orgs`}>
<DropdownSubMenuTrigger
onClick={() => {
setIsOrgSelectOpen(false);
handleOrgNav(org);
}}
className="cursor-pointer font-normal"
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{org.name}</p>
<FontAwesomeIcon className="ml-auto shrink-0" icon={faChevronRight} />
</div>
</DropdownSubMenuTrigger>
<DropdownSubMenuContent
sideOffset={8}
alignOffset={-24}
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
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}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === subOrg.id && (
<FontAwesomeIcon
icon={faCheck}
className="shrink-0 text-primary"
/>
)}
<p className="truncate">{subOrg.name}</p>
</div>
</DropdownMenuItem>
))}
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
<DropdownMenuItem
className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownSubMenuContent>
</DropdownSubMenu>
);
}
return ( return (
<DropdownMenuItem key={org.id}> <DropdownMenuItem
<Button onClick={() => handleOrgNav(org)}
onClick={async () => { className="cursor-pointer font-normal"
if (currentOrg?.id === org.id) return; key={org.id}
>
if (org.authEnforced) { <div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
// org has an org-level auth method enabled (e.g. SAML) {currentOrg?.id === org.id && (
// -> logout + redirect to SAML SSO <FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
await logout.mutateAsync(); <p className="truncate">{org.name}</p>
if (org.orgAuthMethod === AuthMethod.OIDC) { </div>
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);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem> </DropdownMenuItem>
); );
})} })}
@@ -432,6 +440,79 @@ export const Navbar = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{currentOrg.subOrganization && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Link to="/organization/projects">
<Badge
variant="namespace"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faCubes} />
<p className="truncate">{currentOrg.subOrganization.name}</p>
</Badge>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => {
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}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === subOrg.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{subOrg.name}</p>
</div>
</DropdownMenuItem>
))}
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
<DropdownMenuItem
className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
{!isOrgScope && ( {!isOrgScope && (
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
@@ -654,7 +735,11 @@ export const Navbar = () => {
subTitle="Define a new sub-organization under your current organization." subTitle="Define a new sub-organization under your current organization."
> >
<div className="mb-2"> <div className="mb-2">
<NewSubOrganizationForm onClose={() => setShowSubOrgForm(false)} /> <NewSubOrganizationForm
onClose={() => {
setShowSubOrgForm(false);
}}
/>
</div> </div>
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -1,5 +1,7 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -31,9 +33,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
resolver: zodResolver(AddOrgSchema) resolver: zodResolver(AddOrgSchema)
}); });
const navigate = useNavigate();
const queryClient = useQueryClient();
const router = useRouter();
const onSubmit = async ({ name }: FormData) => { const onSubmit = async ({ name }: FormData) => {
try { try {
await createSubOrg.mutateAsync({ const { organization } = await createSubOrg.mutateAsync({
name name
}); });
@@ -42,6 +48,13 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
text: "Successfully created sub organization" text: "Successfully created sub organization"
}); });
onClose(); onClose();
navigate({
to: "/organization/projects",
search: (prev) => ({ ...prev, subOrganization: organization.name })
});
queryClient.clear();
await router.invalidate({ sync: true }).catch(() => null);
} catch { } catch {
createNotification({ createNotification({
text: "Failed to create sub organization", text: "Failed to create sub organization",
@@ -16,6 +16,8 @@ export const OrgNavBar = ({ isHidden }: Props) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const variant = isRootOrganization ? "org" : "namespace";
return ( return (
<> <>
{!isHidden && ( {!isHidden && (
@@ -32,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => {
<TabList className="border-b-0"> <TabList className="border-b-0">
<Link to="/organization/projects"> <Link to="/organization/projects">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
Overview Overview
</Tab> </Tab>
)} )}
</Link> </Link>
<Link to="/organization/app-connections"> <Link to="/organization/app-connections">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
App Connections App Connections
</Tab> </Tab>
)} )}
</Link> </Link>
<Link to="/organization/networking"> <Link to="/organization/networking">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
Networking Networking
</Tab> </Tab>
)} )}
</Link> </Link>
<Link to="/organization/secret-sharing"> <Link to="/organization/secret-sharing">
{({ isActive }) => ( {({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org"> <Tab value={isActive ? "selected" : ""} variant={variant}>
Secret Sharing Secret Sharing
</Tab> </Tab>
)} )}
@@ -61,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
<Link to="/organization/access-management"> <Link to="/organization/access-management">
{({ isActive }) => ( {({ isActive }) => (
<Tab <Tab
variant="org" variant={variant}
value={ value={
isActive || isActive ||
pathname.match( pathname.match(
@@ -77,7 +79,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
</Link> </Link>
<Link to="/organization/audit-logs"> <Link to="/organization/audit-logs">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
Audit Logs Audit Logs
</Tab> </Tab>
)} )}
@@ -85,7 +87,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
{isRootOrganization && ( {isRootOrganization && (
<Link to="/organization/billing"> <Link to="/organization/billing">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
Usage & Billing Usage & Billing
</Tab> </Tab>
)} )}
@@ -93,7 +95,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
)} )}
<Link to="/organization/settings"> <Link to="/organization/settings">
{({ isActive }) => ( {({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}> <Tab variant={variant} value={isActive ? "selected" : ""}>
Settings Settings
</Tab> </Tab>
)} )}
@@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "
export const AccessManagementPage = () => { export const AccessManagementPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const navigate = useNavigate({ const navigate = useNavigate({
from: ROUTE_PATHS.Organization.AccessControlPage.path from: ROUTE_PATHS.Organization.AccessControlPage.path
@@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
</Helmet> </Helmet>
<div className="mx-auto mb-6 w-full max-w-8xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title="Access Control" title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources." description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/> />
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => {
{tabSections {tabSections
.filter((el) => !el.isHidden) .filter((el) => !el.isHidden)
.map((el) => ( .map((el) => (
<Tab variant="org" value={el.key} key={`org-access-tab-${el.key}`}> <Tab
variant={isSubOrganization ? "namespace" : "org"}
value={el.key}
key={`org-access-tab-${el.key}`}
>
{el.label} {el.label}
</Tab> </Tab>
))} ))}
@@ -68,7 +68,7 @@ enum RolesOrderBy {
export const OrgRoleTable = () => { export const OrgRoleTable = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -200,7 +200,9 @@ export const OrgRoleTable = () => {
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-medium text-mineshaft-100">Organization Roles</p> <p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Roles
</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -3,6 +3,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { import {
OrgPermissionAppConnectionActions, OrgPermissionAppConnectionActions,
OrgPermissionSubjects OrgPermissionSubjects
@@ -12,6 +13,8 @@ import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppC
export const AppConnectionsPage = withPermission( export const AppConnectionsPage = withPermission(
() => { () => {
const { isSubOrganization } = useOrganization();
return ( return (
<div className="bg-bunker-800"> <div className="bg-bunker-800">
<Helmet> <Helmet>
@@ -22,7 +25,7 @@ export const AppConnectionsPage = withPermission(
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
className="w-full" className="w-full"
title="App Connections" title="App Connections"
description="Manage organization App Connections" description="Manage organization App Connections"
@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization();
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<Helmet> <Helmet>
@@ -15,7 +18,7 @@ export const AuditLogsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title="Audit Logs" title="Audit Logs"
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> />
@@ -20,7 +20,7 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api"; import { useDeleteGroup } from "@app/hooks/api";
import { useGetGroupById } from "@app/hooks/api/groups/queries"; import { useGetGroupById } from "@app/hooks/api/groups/queries";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -45,6 +45,8 @@ const Page = () => {
const { data, isPending } = useGetGroupById(groupId); const { data, isPending } = useGetGroupById(groupId);
const { isSubOrganization } = useOrganization();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -95,7 +97,11 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Groups Groups
</Link> </Link>
<PageHeader scope="org" description="Organization Group" title={data.group.name}> <PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description={`${isSubOrganization ? "Sub-" : ""}Organization Group`}
title={data.group.name}
>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -29,7 +29,7 @@ const Page = () => {
from: ROUTE_PATHS.Organization.IdentityDetailsByIDPage.id from: ROUTE_PATHS.Organization.IdentityDetailsByIDPage.id
}); });
const identityId = params.identityId as string; const identityId = params.identityId as string;
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId); const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity(); const { mutateAsync: deleteIdentity } = useDeleteIdentity();
@@ -88,7 +88,11 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Identities Identities
</Link> </Link>
<PageHeader scope="org" description="Organization Identity" title={data.identity.name} /> <PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description={`${isSubOrganization ? "Sub-" : ""}Organization Identity`}
title={data.identity.name}
/>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection <IdentityDetailsSection
@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup"; import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
export const NetworkingPage = () => { export const NetworkingPage = () => {
const { isSubOrganization } = useOrganization();
return ( return (
<> <>
<Helmet> <Helmet>
@@ -14,7 +17,7 @@ export const NetworkingPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title="Networking" title="Networking"
description="Manage gateways and relays to securely access private network resources from Infisical" description="Manage gateways and relays to securely access private network resources from Infisical"
/> />
@@ -2,6 +2,7 @@ import { useState } from "react";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { GatewayTab } from "../GatewayTab/GatewayTab"; import { GatewayTab } from "../GatewayTab/GatewayTab";
import { RelayTab } from "../RelayTab/RelayTab"; import { RelayTab } from "../RelayTab/RelayTab";
@@ -18,11 +19,13 @@ export const NetworkingTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
const { isSubOrganization } = useOrganization();
return ( return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}> <Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
{tab.name} {tab.name}
</Tab> </Tab>
))} ))}
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { NewProjectModal } from "@app/components/projects"; import { NewProjectModal } from "@app/components/projects";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView"; import { AllProjectView } from "./components/AllProjectView";
@@ -52,7 +52,7 @@ export const ProjectsPage = () => {
] as const); ] as const);
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { isSubOrganization } = useOrganization();
const isAddingProjectsAllowed = subscription?.workspaceLimit const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit
: true; : true;
@@ -64,7 +64,7 @@ export const ProjectsPage = () => {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title="Overview" title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them." description="Your team's complete security toolkit - organized and ready when you need them."
/> />
@@ -30,7 +30,7 @@ export const Page = () => {
from: ROUTE_PATHS.Organization.RoleByIDPage.id from: ROUTE_PATHS.Organization.RoleByIDPage.id
}); });
const roleId = params.roleId as string; const roleId = params.roleId as string;
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId); const { data } = useGetOrgRole(orgId, roleId);
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
@@ -91,7 +91,7 @@ export const Page = () => {
Roles Roles
</Link> </Link>
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title={data.name} title={data.name}
description={ description={
<> <>
@@ -4,12 +4,13 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ShareSecretSection } from "./ShareSecretSection"; import { ShareSecretSection } from "./ShareSecretSection";
export const SecretSharingPage = () => { export const SecretSharingPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return ( return (
<> <>
<Helmet> <Helmet>
@@ -22,7 +23,7 @@ export const SecretSharingPage = () => {
<div className="h-full"> <div className="h-full">
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white"> <div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title="Secret Sharing" title="Secret Sharing"
description="Share secrets securely using a shareable link" description="Share secrets securely using a shareable link"
> >
@@ -3,6 +3,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab"; import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab"; import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
@@ -14,6 +15,7 @@ enum SecretSharingPageTabs {
export const ShareSecretSection = () => { export const ShareSecretSection = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { isSubOrganization } = useOrganization();
const { selectedTab } = useSearch({ const { selectedTab } = useSearch({
from: ROUTE_PATHS.Organization.SecretSharing.id from: ROUTE_PATHS.Organization.SecretSharing.id
@@ -26,6 +28,8 @@ export const ShareSecretSection = () => {
}); });
}; };
const tabVariant = isSubOrganization ? "namespace" : "org";
return ( return (
<div> <div>
<Helmet> <Helmet>
@@ -36,10 +40,10 @@ export const ShareSecretSection = () => {
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab variant="org" value={SecretSharingPageTabs.ShareSecret}> <Tab variant={tabVariant} value={SecretSharingPageTabs.ShareSecret}>
Share Secrets Share Secrets
</Tab> </Tab>
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}> <Tab variant={tabVariant} value={SecretSharingPageTabs.RequestSecret}>
Request Secrets Request Secrets
</Tab> </Tab>
</TabList> </TabList>
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { import {
OrgPermissionSecretShareAction, OrgPermissionSecretShareAction,
OrgPermissionSubjects OrgPermissionSubjects
@@ -13,6 +14,7 @@ import { SecretSharingSettingsTabGroup } from "./components";
export const SecretSharingSettingsPage = withPermission( export const SecretSharingSettingsPage = withPermission(
() => { () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return ( return (
<> <>
@@ -21,7 +23,10 @@ export const SecretSharingSettingsPage = withPermission(
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader scope="org" title="Secret Share Settings" /> <PageHeader
scope={isSubOrganization ? "namespace" : "org"}
title="Secret Share Settings"
/>
<SecretSharingSettingsTabGroup /> <SecretSharingSettingsTabGroup />
</div> </div>
</div> </div>
@@ -2,11 +2,13 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgTabGroup } from "./components"; import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return ( return (
<> <>
@@ -16,7 +18,7 @@ export const SettingsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white"> <div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings" description="Configure organization-wide settings"
title={t("settings.org.title")} title={t("settings.org.title")}
/> />
@@ -74,7 +74,7 @@ export const OrgTabGroup = () => {
{tabs {tabs
.filter((el) => !el.isHidden) .filter((el) => !el.isHidden)
.map((tab) => ( .map((tab) => (
<Tab variant="org" value={tab.key} key={tab.key}> <Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
{tab.name} {tab.name}
</Tab> </Tab>
))} ))}
@@ -46,7 +46,7 @@ const Page = withPermission(
}); });
const membershipId = search.membershipId as string; const membershipId = search.membershipId as string;
const { user } = useUser(); const { user } = useUser();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const userId = user?.id || ""; const userId = user?.id || "";
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -131,7 +131,7 @@ const Page = withPermission(
Users Users
</Link> </Link>
<PageHeader <PageHeader
scope="org" scope={isSubOrganization ? "namespace" : "org"}
title={ title={
membership.user.firstName || membership.user.lastName membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim() ? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
@@ -140,7 +140,7 @@ const Page = withPermission(
membership.inviteEmail ?? membership.inviteEmail ??
"Unknown User") "Unknown User")
} }
description="Organization User Membership" description={`${isSubOrganization ? "Sub-" : ""}Organization User Membership`}
> >
<div> <div>
{userId !== membership.user.id && ( {userId !== membership.user.id && (
@@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => {
} }
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-screen items-center justify-center bg-bunker-800">
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200"> <div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" /> <FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
<p> <p>