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

View File

@@ -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) => {

View File

@@ -24,7 +24,7 @@ const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { 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 }
};

View File

@@ -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);
}
}
}

View File

@@ -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;
},

View File

@@ -4,6 +4,7 @@ export type TSubOrganization = {
slug: string;
createdAt: string;
updatedAt: string;
parentOrgId: string;
};
export type TCreateSubOrganizationDTO = {

View File

@@ -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 (
<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">
@@ -253,38 +284,46 @@ export const Navbar = () => {
) : (
<>
<div className="flex items-center overflow-hidden">
<DropdownMenu modal={false}>
<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">
<Badge
variant="org"
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={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>
{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>
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
onClick={async () => {
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"
)}
>
<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>
</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>
<div>
<IconButton
@@ -298,127 +337,96 @@ export const Navbar = () => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
align="center"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
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">
organizations
Organizations
</div>
{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 (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
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);
}}
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
onClick={() => handleOrgNav(org)}
className="cursor-pointer font-normal"
key={org.id}
>
<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>
</div>
</DropdownMenuItem>
);
})}
@@ -432,6 +440,79 @@ export const Navbar = () => {
</DropdownMenuContent>
</DropdownMenu>
</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 && (
<>
<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."
>
<div className="mb-2">
<NewSubOrganizationForm onClose={() => setShowSubOrgForm(false)} />
<NewSubOrganizationForm
onClose={() => {
setShowSubOrgForm(false);
}}
/>
</div>
</ModalContent>
</Modal>

View File

@@ -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",

View File

@@ -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) => {
<TabList className="border-b-0">
<Link to="/organization/projects">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Overview
</Tab>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
App Connections
</Tab>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Networking
</Tab>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org">
<Tab value={isActive ? "selected" : ""} variant={variant}>
Secret Sharing
</Tab>
)}
@@ -61,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
<Link to="/organization/access-management">
{({ isActive }) => (
<Tab
variant="org"
variant={variant}
value={
isActive ||
pathname.match(
@@ -77,7 +79,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Audit Logs
</Tab>
)}
@@ -85,7 +87,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
{isRootOrganization && (
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
@@ -93,7 +95,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
)}
<Link to="/organization/settings">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
<Tab variant={variant} value={isActive ? "selected" : ""}>
Settings
</Tab>
)}

View File

@@ -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 = () => {
</Helmet>
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Access Control"
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
/>
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => {
{tabSections
.filter((el) => !el.isHidden)
.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}
</Tab>
))}

View File

@@ -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 (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<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}>
{(isAllowed) => (
<Button

View File

@@ -3,6 +3,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
@@ -12,6 +13,8 @@ import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppC
export const AppConnectionsPage = withPermission(
() => {
const { isSubOrganization } = useOrganization();
return (
<div className="bg-bunker-800">
<Helmet>
@@ -22,7 +25,7 @@ export const AppConnectionsPage = withPermission(
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
className="w-full"
title="App Connections"
description="Manage organization App Connections"

View File

@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { LogsSection } from "./components";
export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization();
return (
<div className="h-full bg-bunker-800">
<Helmet>
@@ -15,7 +18,7 @@ export const AuditLogsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Audit Logs"
description="Audit logs for security and compliance teams to monitor information access."
/>

View File

@@ -20,7 +20,7 @@ import {
Tooltip
} from "@app/components/v2";
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 { useGetGroupById } from "@app/hooks/api/groups/queries";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -45,6 +45,8 @@ const Page = () => {
const { data, isPending } = useGetGroupById(groupId);
const { isSubOrganization } = useOrganization();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -95,7 +97,11 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} />
Groups
</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>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">

View File

@@ -29,7 +29,7 @@ const Page = () => {
from: ROUTE_PATHS.Organization.IdentityDetailsByIDPage.id
});
const identityId = params.identityId as string;
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
@@ -88,7 +88,11 @@ const Page = () => {
<FontAwesomeIcon icon={faChevronLeft} />
Identities
</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="mr-4 w-96">
<IdentityDetailsSection

View File

@@ -1,10 +1,13 @@
import { Helmet } from "react-helmet";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
export const NetworkingPage = () => {
const { isSubOrganization } = useOrganization();
return (
<>
<Helmet>
@@ -14,7 +17,7 @@ export const NetworkingPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Networking"
description="Manage gateways and relays to securely access private network resources from Infisical"
/>

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { GatewayTab } from "../GatewayTab/GatewayTab";
import { RelayTab } from "../RelayTab/RelayTab";
@@ -18,11 +19,13 @@ export const NetworkingTabGroup = () => {
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
const { isSubOrganization } = useOrganization();
return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{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>
))}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { NewProjectModal } from "@app/components/projects";
import { PageHeader } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { useOrganization, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { AllProjectView } from "./components/AllProjectView";
@@ -52,7 +52,7 @@ export const ProjectsPage = () => {
] as const);
const { subscription } = useSubscription();
const { isSubOrganization } = useOrganization();
const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit
: true;
@@ -64,7 +64,7 @@ export const ProjectsPage = () => {
<link rel="icon" href="/infisical.ico" />
</Helmet>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Overview"
description="Your team's complete security toolkit - organized and ready when you need them."
/>

View File

@@ -30,7 +30,7 @@ export const Page = () => {
from: ROUTE_PATHS.Organization.RoleByIDPage.id
});
const roleId = params.roleId as string;
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId);
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
@@ -91,7 +91,7 @@ export const Page = () => {
Roles
</Link>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title={data.name}
description={
<>

View File

@@ -4,12 +4,13 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ShareSecretSection } from "./ShareSecretSection";
export const SecretSharingPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return (
<>
<Helmet>
@@ -22,7 +23,7 @@ export const SecretSharingPage = () => {
<div className="h-full">
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title="Secret Sharing"
description="Share secrets securely using a shareable link"
>

View File

@@ -3,6 +3,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useOrganization } from "@app/context";
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
@@ -14,6 +15,7 @@ enum SecretSharingPageTabs {
export const ShareSecretSection = () => {
const navigate = useNavigate();
const { isSubOrganization } = useOrganization();
const { selectedTab } = useSearch({
from: ROUTE_PATHS.Organization.SecretSharing.id
@@ -26,6 +28,8 @@ export const ShareSecretSection = () => {
});
};
const tabVariant = isSubOrganization ? "namespace" : "org";
return (
<div>
<Helmet>
@@ -36,10 +40,10 @@ export const ShareSecretSection = () => {
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList>
<Tab variant="org" value={SecretSharingPageTabs.ShareSecret}>
<Tab variant={tabVariant} value={SecretSharingPageTabs.ShareSecret}>
Share Secrets
</Tab>
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}>
<Tab variant={tabVariant} value={SecretSharingPageTabs.RequestSecret}>
Request Secrets
</Tab>
</TabList>

View File

@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
OrgPermissionSecretShareAction,
OrgPermissionSubjects
@@ -13,6 +14,7 @@ import { SecretSharingSettingsTabGroup } from "./components";
export const SecretSharingSettingsPage = withPermission(
() => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return (
<>
@@ -21,7 +23,10 @@ export const SecretSharingSettingsPage = withPermission(
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader scope="org" title="Secret Share Settings" />
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
title="Secret Share Settings"
/>
<SecretSharingSettingsTabGroup />
</div>
</div>

View File

@@ -2,11 +2,13 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgTabGroup } from "./components";
export const SettingsPage = () => {
const { t } = useTranslation();
const { isSubOrganization } = useOrganization();
return (
<>
@@ -16,7 +18,7 @@ export const SettingsPage = () => {
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-8xl">
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings"
title={t("settings.org.title")}
/>

View File

@@ -74,7 +74,7 @@ export const OrgTabGroup = () => {
{tabs
.filter((el) => !el.isHidden)
.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>
))}

View File

@@ -46,7 +46,7 @@ const Page = withPermission(
});
const membershipId = search.membershipId as string;
const { user } = useUser();
const { currentOrg } = useOrganization();
const { currentOrg, isSubOrganization } = useOrganization();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
@@ -131,7 +131,7 @@ const Page = withPermission(
Users
</Link>
<PageHeader
scope="org"
scope={isSubOrganization ? "namespace" : "org"}
title={
membership.user.firstName || membership.user.lastName
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
@@ -140,7 +140,7 @@ const Page = withPermission(
membership.inviteEmail ??
"Unknown User")
}
description="Organization User Membership"
description={`${isSubOrganization ? "Sub-" : ""}Organization User Membership`}
>
<div>
{userId !== membership.user.id && (

View File

@@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => {
}
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">
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
<p>